Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
feec99c
feat: modernize email templates with better-svelte-email
niemyjski May 27, 2026
d608704
feat: modernize email templates with Svelte 5, Tailwind, Storybook
niemyjski May 27, 2026
12b1547
fix: correct Storybook rendering bugs and text formatting issues
niemyjski May 27, 2026
9829688
fix(email-templates): address PR feedback and staff engineer review
niemyjski May 28, 2026
55af4df
fix: address all PR review feedback
niemyjski May 30, 2026
3c03537
fix: correct Fields sample data format in Storybook preview
niemyjski May 30, 2026
ffc43ef
test: add body content assertions to all mailer tests and cover IsReg…
niemyjski May 30, 2026
1b639ca
docs: add before/after email template comparison screenshots from Mai…
niemyjski May 30, 2026
fbd0e31
refactor: replace raw HTML stats table with Row/Column components in …
niemyjski May 30, 2026
07dbfb8
chore: remove nodemailer test dependency, add .gstack to .gitignore
niemyjski May 30, 2026
b25c77a
Merge origin/main into niemyjski/email-templates-modernization
niemyjski Jul 12, 2026
3700bbf
fix(email): harden template production readiness
niemyjski Jul 12, 2026
84775c5
Merge origin/main into niemyjski/email-templates-modernization
niemyjski Jul 16, 2026
5d3a58a
fix(email): keep template tooling in sync
niemyjski Jul 16, 2026
aae222a
Merge origin/main into niemyjski/email-templates-modernization
niemyjski Jul 26, 2026
7a93703
fix(email): harden template tooling after merge
niemyjski Jul 26, 2026
75e915c
feat: add email template parity validation
niemyjski Jul 26, 2026
b09ecad
ci: stabilize email pixel parity
niemyjski Jul 26, 2026
270cbfa
fix(email): close reviewer feedback audit
niemyjski Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ updates:
interval: "weekly"
cooldown:
default-days: 7

- package-ecosystem: "npm"
directory: "/src/Exceptionless.EmailTemplates"
schedule:
interval: "weekly"
cooldown:
default-days: 7
62 changes: 61 additions & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,66 @@ jobs:
- name: Run Integration Tests
run: echo "npm run test:integration"

test-email-templates:
# The legacy templates specify Helvetica. Keep exact pixel comparisons on
# macOS, where that production font is available, instead of comparing
# different Linux fallback-font table metrics.
runs-on: macos-latest
timeout-minutes: 15
defaults:
run:
working-directory: src/Exceptionless.EmailTemplates

steps:
- name: Checkout
uses: actions/checkout@v7
with:
ref: ${{ (github.event_name == 'repository_dispatch' && github.event.client_payload.head_sha) || github.event.pull_request.head.sha || github.sha }}

- name: Setup Node.js environment
uses: actions/setup-node@v6
with:
node-version: 24

- name: Cache npm packages
uses: actions/cache@v6
id: cache-email-template-npm
with:
path: ~/.npm
key: npm-email-${{ runner.os }}-${{ hashFiles('src/Exceptionless.EmailTemplates/package-lock.json') }}

- name: Install Npm Packages
run: npm ci

- name: Lint
run: npm run lint

- name: Type Check
run: npm run check

- name: Validate Legacy Text, Actions, and Pixels
run: npm run validate:parity

- name: Upload Email Parity Failures
if: failure()
uses: actions/upload-artifact@v5
with:
name: email-parity-failures
path: src/Exceptionless.EmailTemplates/parity-artifacts/
if-no-files-found: ignore

- name: Build Storybook
run: npm run build-storybook

- name: Verify Generated HTML is Up-To-Date
run: |
changes="$(git status --short --untracked-files=all -- ../Exceptionless.Core/Mail/Templates/ generated-templates.json)"
if [ -n "$changes" ]; then
echo "$changes"
echo "::error::Generated email artifacts are stale. Run 'npm run build' in src/Exceptionless.EmailTemplates and commit the updated files."
exit 1
fi

test-e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
Expand Down Expand Up @@ -437,7 +497,7 @@ jobs:
docker-publish:
if: ${{ needs.version.outputs.should_publish == 'true' }}
runs-on: ubuntu-latest
needs: [version, docker-build, test-api, test-client, test-e2e]
needs: [version, docker-build, test-api, test-client, test-email-templates, test-e2e]
timeout-minutes: 30
env:
VERSION: ${{ needs.version.outputs.version }}
Expand Down
10 changes: 7 additions & 3 deletions src/Exceptionless.AppHost/Extensions/WorktreeScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ public sealed record WorktreePorts(
int OldAppHttps,
int OldAppLiveReload,
int AppHttps,
int DocsHttp)
int DocsHttp,
int Storybook,
int EmailStorybook)
{
public string ApiHttpUrl => $"http://localhost:{ApiHttp}";
public string ApiHttpsUrl => $"https://localhost:{ApiHttps}";
Expand Down Expand Up @@ -50,7 +52,7 @@ public static class WorktreeScope

public static WorktreePorts AssignFreePorts()
{
int[] ports = FreePorts(12);
int[] ports = FreePorts(14);
var assignments = new WorktreePorts(
ports[0],
ports[1],
Expand All @@ -63,7 +65,9 @@ public static WorktreePorts AssignFreePorts()
ports[8],
ports[9],
ports[10],
ports[11]);
ports[11],
ports[12],
ports[13]);

Environment.SetEnvironmentVariable("ASPNETCORE_URLS", $"https://localhost:{assignments.DashboardHttps};http://localhost:{assignments.DashboardHttp}");
Environment.SetEnvironmentVariable("DOTNET_DASHBOARD_OTLP_ENDPOINT_URL", $"https://localhost:{assignments.DashboardOtlp}");
Expand Down
24 changes: 24 additions & 0 deletions src/Exceptionless.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
string oldAppAspNetCoreUrls = String.Concat("http://localhost:", oldAppHttpPort);
int appPort = worktreePorts?.AppHttps ?? 7131;
int docsPort = worktreePorts?.DocsHttp ?? 7141;
int storybookPort = worktreePorts?.Storybook ?? 6006;
int emailStorybookPort = worktreePorts?.EmailStorybook ?? 6008;
const int DefaultApiHttpsPort = 7111;
string exceptionlessServerUrl = worktreePorts?.ApiHttpsUrl ?? $"https://api-ex.dev.localhost:{DefaultApiHttpsPort}";
const string SharedEmailConnectionString = "smtp://localhost:1026";
Expand Down Expand Up @@ -224,6 +226,28 @@
u.DisplayOrder = 100;
})
.WithParentRelationship(api);

builder.AddJavaScriptApp("Storybook", "../Exceptionless.Web/ClientApp", "storybook")
.WithRunScript("storybook", ["--", "--port", storybookPort.ToString(), "--no-open"])
.WithHttpEndpoint(port: storybookPort, targetPort: storybookPort, name: "http", isProxied: false)
.WithUrlForEndpoint("http", u =>
{
u.DisplayText = "Component Library";
u.DisplayOrder = 200;
})
.WithHttpHealthCheck("/")
.WithParentRelationship(api);

builder.AddJavaScriptApp("EmailStorybook", "../Exceptionless.EmailTemplates", "storybook")
.WithRunScript("storybook", ["--", "--port", emailStorybookPort.ToString(), "--no-open"])
.WithHttpEndpoint(port: emailStorybookPort, targetPort: emailStorybookPort, name: "http", isProxied: false)
.WithUrlForEndpoint("http", u =>
{
u.DisplayText = "Email Templates";
u.DisplayOrder = 300;
})
.WithHttpHealthCheck("/")
.WithParentRelationship(api);
}
#pragma warning restore ASPIREBROWSERLOGS001
}
Expand Down
12 changes: 11 additions & 1 deletion src/Exceptionless.Core/Mail/Mailer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ private void AddUserInfo(PersistentEvent ev, Dictionary<string, object?> data)
data["UserDescription"] = ud.Description;

if (!String.IsNullOrEmpty(ud?.EmailAddress))
{
data["UserEmail"] = ud.EmailAddress;
data["UserEmailHref"] = BuildMailtoHref(ud.EmailAddress, ud.Description);
}

string? displayName = null;
if (!String.IsNullOrEmpty(ui?.Identity))
Expand All @@ -135,6 +138,12 @@ private void AddUserInfo(PersistentEvent ev, Dictionary<string, object?> data)
data["HasUserInfo"] = ud is not null || ui is not null;
}

private static string BuildMailtoHref(string emailAddress, string? body)
{
string href = $"mailto:{Uri.EscapeDataString(emailAddress)}";
return String.IsNullOrEmpty(body) ? href : $"{href}?body={Uri.EscapeDataString(body)}";
}

private static void AddDefaultFields(PersistentEvent ev, Dictionary<string, object?> data)
{
if (ev.Tags?.Count > 0)
Expand Down Expand Up @@ -269,7 +278,8 @@ private static IEnumerable<object> GetStackTemplateData(IEnumerable<Stack> stack
StackId = s.Id,
Title = s.Title.Truncate(50),
TypeName = s.GetTypeName()?.Truncate(50),
s.Status
s.Status,
IsRegressed = s.Status == StackStatus.Regressed
});
}

Expand Down
Loading
Loading