Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
143 changes: 7 additions & 136 deletions server/lib/orcasite/notifications/email.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,76 +28,7 @@ defmodule Orcasite.Notifications.Email do
url(~p"/s/subscription/unsubscribe?token=#{assigns.unsubscribe_token}")
)

"""
<mjml>
<mj-body>
<mj-section>
<mj-column>

<mj-text font-size="20px" font-family="helvetica">
A new detection has been submitted at {{ node_name }} ({{ node }})!
</mj-text>

<mj-text font-size="20px" font-family="helvetica">
Description: {{#if meta["description"]}}{{ meta["description"] }}{{else}}(no description){{/if}}
</mj-text>
{{#if meta["listener_count"] }}
<mj-text font-size="20px" font-family="helvetica">
Listeners: {{ meta["listener_count"] }}
</mj-text>
{{/if}}
{{#if meta["candidate_id"] }}
<mj-text font-size="20px" font-family="helvetica">
Review here: <a href="https://live.orcasound.net/reports/{{meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">{{ meta["candidate_id"] }}</a>
</mj-text>
{{/if}}
{{#if node && meta["start_time"] && meta["category"] }}
<mj-text font-size="20px" font-family="helvetica">
<a href="https://live.orcasound.net/bouts/new/{{ node }}?time={{ meta["start_time"] }}&category={{ meta["category"] }}&utm_source=email&utm_medium=email&utm_campaign=notifications">Start a new bout</a>
</mj-text>
{{/if}}

{{#if notifications_since_count > 0}}
<mj-text font-size="20px" font-family="helvetica">
There have been {{ notifications_since_count }} other detections since the last notification.
</mj-text>
<mj-table>
<tr style="border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<th style="padding: 0 5px 0 0;">Feed</th>
<th style="padding: 0 15px; text-align: right;">#</th>
<th style="padding: 0 0 0 15px;">Description</th>
<th style="padding: 0 0 0 15px;">Action</th>
</tr>
{{#each notifications_since as |notif_meta|}}
<tr>
<td style="padding: 0 5px 0 0;white-space:nowrap;">{{ notif_meta["node"] }}</td>
<td style="padding: 0 15px;text-align:right;">{{ notif_meta["listener_count"] }}</td>
<td style="padding: 0 0 0 15px;">{{ notif_meta["description"] }}</td>
<td style="padding: 0 0 0 15px;">
{{#if notif_meta["candidate_id"] }}
<a href="https://live.orcasound.net/reports/{{meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">Review</a>
{{/if}}
</td>
</tr>
{{/each}}
</mj-table>
{{/if}}


<mj-text font-size="20px">
Listen here: <a href="https://live.orcasound.net/listen/{{node}}?utm_source=email&utm_medium=email&utm_campaign=notifications">https://live.orcasound.net/listen/{{ node }}</a>
</mj-text>

{{#if unsubscribe_token }}
<mj-text font-size="20px">
If you no longer wish to receive these emails, you can <a href="{{unsubscribe_url}}">unsubscribe</a>.
</mj-text>
{{/if}}
</mj-column>
</mj-section>
</mj-body>
</mjml>
"""
read_template("new_detection")
|> compile_mjml(assigns)
end

Expand All @@ -109,72 +40,7 @@ defmodule Orcasite.Notifications.Email do
url(~p"/s/subscription/unsubscribe?token=#{assigns.unsubscribe_token}")
)

"""
<mjml>
<mj-body>
<!-- header -->
<mj-section padding="0" full-width="" background-color="#c4cdd3">
<mj-column padding="0">
<mj-image src="https://orcasite.s3.us-west-2.amazonaws.com/email_assets/orcasound_email_header.jpg" alt="Orcasound" align="center" container-background-color="#c4cdd3"></mj-image>
</mj-column>
</mj-section>

<mj-section>
<mj-column background-color="#404040" padding="18px">
<mj-text font-size="18px" color="#F2F2F2" font-family="Helvetica" line-height="150%">
{{ meta["message"] }}
</mj-text>
</mj-column>
</mj-section>

<mj-section>
<mj-column>
<mj-image src="https://orcasite.s3.us-west-2.amazonaws.com/email_assets/orcasound_dont_miss.jpg"></mj-image>

<mj-button href="http://live.orcasound.net/listen/{{ node }}?utm_source=email&utm_medium=email&utm_campaign=notifications" background-color="#0F0F0F" border-radius="31px" font-size="18px" padding="18px" font-weight="bold">LISTEN NOW!</mj-button>
</mj-column>
</mj-section>

<mj-section>
<mj-column background-color="#404040">
<mj-text color="#F2F2F2" font-size="16px" align="center" line-height="150%" font-family="Helvetica">
If you miss the concert, <br /> watch the <a href="https://orcasound.net/blog?ecm&utm_source=email&utm_medium=email&utm_campaign=notifications" style="color: #007C89;">Orcasound blog</a> for recordings & bioacoustic analysis!
</mj-text>
</mj-column>
</mj-section>

<!-- footer -->
<mj-section background-color="#5d5b72" full-width="">
<mj-column>
<mj-text font-weight="bold" font-family="Helvetica" color="#ffffff" align="center" line-height="150%">
If you encounter whales at sea, <a href="https://www.bewhalewise.org/" style="font-weight: normal; color: #ffffff">Be Whale Wise</a>.<br />
Know the laws and best practices in both the U.S. and Canada.
</mj-text>

<mj-divider border-color="#404040" border-width="1px"></mj-divider>

<mj-text color="#ffffff" font-size="12px" font-style="italic" align="center" line-height="150%">
Copyright © 2023 Orcasound, All rights reserved. <br />
You are receiving this email because you opted in via our website.
</mj-text>
<mj-text color="#ffffff" font-size="12px" align="center" line-height="150%">
<strong>Our mailing address is:</strong><br />
Orcasound<br />
7044 17th Ave NE<br />
Seattle, WA 98115-5739
</mj-text>
{{#if unsubscribe_token }}
<mj-text color="#ffffff" font-size="12px" align="center" line-height="150%">
If you no longer wish to receive these emails,<br />
you can <a href="{{ unsubscribe_url }}" style="color: #ffffff;">unsubscribe</a>.
</mj-text>
{{/if}}
</mj-column>
</mj-section>

</mj-body>
</mjml>
"""
read_template("confirmed_candidate")
|> compile_mjml(assigns)
end

Expand All @@ -199,6 +65,11 @@ defmodule Orcasite.Notifications.Email do
)
end

defp read_template(name) do
Path.expand("lib/orcasite/notifications/templates/#{name}.mjml.eex")
|> File.read!()
end
Comment on lines +68 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Make template path robust to CWD/release layout.

Hardcoding "lib/..." depends on process CWD. Resolve relative to this module instead.

-  defp read_template(name) do
-    Path.expand("lib/orcasite/notifications/templates/#{name}.mjml.eex")
-    |> File.read!()
-  end
+  defp read_template(name) do
+    Path.expand("templates/#{name}.mjml.eex", __DIR__)
+    |> File.read!()
+  end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defp read_template(name) do
Path.expand("lib/orcasite/notifications/templates/#{name}.mjml.eex")
|> File.read!()
end
defp read_template(name) do
Path.expand("templates/#{name}.mjml.eex", __DIR__)
|> File.read!()
end
🤖 Prompt for AI Agents
In server/lib/orcasite/notifications/email.ex around lines 68–71, the template
path is hardcoded to "lib/..." which depends on the process CWD; change it to
build the path relative to this module file (use __DIR__ or similar) and join
into the templates folder so it works in releases/other CWDs—i.e. construct the
full path from the current file directory plus "templates/#{name}.mjml.eex"
(using Path.join/1 or Path.expand with __DIR__), then read that file with
File.read!().


def compile_mjml(mjml, assigns) do
mjml
|> Zappa.compile!()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<mjml>
<mj-body>
<!-- header -->
<mj-section padding="0" full-width="" background-color="#c4cdd3">
<mj-column padding="0">
<mj-image src="https://orcasite.s3.us-west-2.amazonaws.com/email_assets/orcasound_email_header.jpg" alt="Orcasound" align="center" container-background-color="#c4cdd3"></mj-image>
</mj-column>
</mj-section>

<mj-section>
<mj-column background-color="#404040" padding="18px">
<mj-text font-size="18px" color="#F2F2F2" font-family="Helvetica" line-height="150%">
{{ meta["message"] }}
</mj-text>
</mj-column>
</mj-section>

<mj-section>
<mj-column>
<mj-image src="https://orcasite.s3.us-west-2.amazonaws.com/email_assets/orcasound_dont_miss.jpg"></mj-image>

<mj-button href="http://live.orcasound.net/listen/{{ node }}?utm_source=email&utm_medium=email&utm_campaign=notifications" background-color="#0F0F0F" border-radius="31px" font-size="18px" padding="18px" font-weight="bold">LISTEN NOW!</mj-button>
</mj-column>
</mj-section>

<mj-section>
<mj-column background-color="#404040">
<mj-text color="#F2F2F2" font-size="16px" align="center" line-height="150%" font-family="Helvetica">
If you miss the concert, <br /> watch the <a href="https://orcasound.net/blog?ecm&utm_source=email&utm_medium=email&utm_campaign=notifications" style="color: #007C89;">Orcasound blog</a> for recordings & bioacoustic analysis!
</mj-text>
</mj-column>
</mj-section>

<!-- footer -->
<mj-section background-color="#5d5b72" full-width="">
<mj-column>
<mj-text font-weight="bold" font-family="Helvetica" color="#ffffff" align="center" line-height="150%">
If you encounter whales at sea, <a href="https://www.bewhalewise.org/" style="font-weight: normal; color: #ffffff">Be Whale Wise</a>.<br />
Know the laws and best practices in both the U.S. and Canada.
</mj-text>

<mj-divider border-color="#404040" border-width="1px"></mj-divider>

<mj-text color="#ffffff" font-size="12px" font-style="italic" align="center" line-height="150%">
Copyright © 2023 Orcasound, All rights reserved. <br />
You are receiving this email because you opted in via our website.
</mj-text>
<mj-text color="#ffffff" font-size="12px" align="center" line-height="150%">
<strong>Our mailing address is:</strong><br />
Orcasound<br />
7044 17th Ave NE<br />
Seattle, WA 98115-5739
</mj-text>
{{#if unsubscribe_token }}
<mj-text color="#ffffff" font-size="12px" align="center" line-height="150%">
If you no longer wish to receive these emails,<br />
you can <a href="{{ unsubscribe_url }}" style="color: #ffffff;">unsubscribe</a>.
</mj-text>
{{/if}}
</mj-column>
</mj-section>

</mj-body>
</mjml>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<mjml>
<mj-body>
<mj-section>
<mj-column>

<mj-text font-size="20px" font-family="helvetica">
A new detection has been submitted at {{ node_name }} ({{ node }})!
</mj-text>

<mj-text font-size="20px" font-family="helvetica">
Description: {{#if meta["description"]}}{{ meta["description"] }}{{else}}(no description){{/if}}
</mj-text>
{{#if meta["listener_count"] }}
<mj-text font-size="20px" font-family="helvetica">
Listeners: {{ meta["listener_count"] }}
</mj-text>
{{/if}}
{{#if meta["candidate_id"] }}
<mj-text font-size="20px" font-family="helvetica">
Review here: <a href="https://live.orcasound.net/reports/{{meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">{{ meta["candidate_id"] }}</a>
</mj-text>
{{/if}}
{{#if node && meta["start_time"] && meta["category"] }}
<mj-text font-size="20px" font-family="helvetica">
<a href="https://live.orcasound.net/bouts/new/{{ node }}?time={{ meta["start_time"] }}&category={{ meta["category"] }}&utm_source=email&utm_medium=email&utm_campaign=notifications">Start a new bout</a>
</mj-text>
{{/if}}

{{#if notifications_since_count > 0}}
<mj-text font-size="20px" font-family="helvetica">
There have been {{ notifications_since_count }} other detections since the last notification.
</mj-text>
<mj-table>
<tr style="border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
<th style="padding: 0 5px 0 0;">Feed</th>
<th style="padding: 0 15px; text-align: right;">#</th>
<th style="padding: 0 0 0 15px;">Description</th>
<th style="padding: 0 0 0 15px;">Action</th>
</tr>
{{#each notifications_since as |notif_meta|}}
<tr>
<td style="padding: 0 5px 0 0;white-space:nowrap;">{{ notif_meta["node"] }}</td>
<td style="padding: 0 15px;text-align:right;">{{ notif_meta["listener_count"] }}</td>
<td style="padding: 0 0 0 15px;">{{ notif_meta["description"] }}</td>
<td style="padding: 0 0 0 15px;">
{{#if notif_meta["candidate_id"] }}
<a href="https://live.orcasound.net/reports/{{meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">Review</a>
Copy link

Copilot AI Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable reference is incorrect. It should use notif_meta["candidate_id"] instead of meta["candidate_id"] since this is inside the {{#each notifications_since as |notif_meta|}} loop.

Suggested change
<a href="https://live.orcasound.net/reports/{{meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">Review</a>
<a href="https://live.orcasound.net/reports/{{notif_meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">Review</a>

Copilot uses AI. Check for mistakes.
{{/if}}
Comment on lines +46 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Wrong variable in Review link inside the loop.

Links will point to the outer meta id, not the per‑row notif_meta id.

-              <a href="https://live.orcasound.net/reports/{{meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">Review</a>
+              <a href="https://live.orcasound.net/reports/{{notif_meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">Review</a>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{{#if notif_meta["candidate_id"] }}
<a href="https://live.orcasound.net/reports/{{meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">Review</a>
{{/if}}
{{#if notif_meta["candidate_id"] }}
<a href="https://live.orcasound.net/reports/{{notif_meta["candidate_id"]}}?utm_source=email&utm_medium=email&utm_campaign=notifications">Review</a>
{{/if}}
🤖 Prompt for AI Agents
In server/lib/orcasite/notifications/templates/new_detection.mjml.eex around
lines 46 to 48, the Review link uses the outer meta["candidate_id"] instead of
the per-row notif_meta["candidate_id"]; change the URL interpolation to use
notif_meta["candidate_id"] (so the href becomes
.../reports/{{notif_meta["candidate_id"]}}?...), ensuring it matches the
existing {{#if notif_meta["candidate_id"] }} guard and preserves the utm query
params and quoting style.

</td>
</tr>
{{/each}}
</mj-table>
{{/if}}


<mj-text font-size="20px">
Listen here: <a href="https://live.orcasound.net/listen/{{node}}?utm_source=email&utm_medium=email&utm_campaign=notifications">https://live.orcasound.net/listen/{{ node }}</a>
</mj-text>

{{#if unsubscribe_token }}
<mj-text font-size="20px">
If you no longer wish to receive these emails, you can <a href="{{unsubscribe_url}}">unsubscribe</a>.
</mj-text>
{{/if}}
</mj-column>
</mj-section>
</mj-body>
</mjml>
40 changes: 40 additions & 0 deletions server/test/orcasite/notifications/email_template_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule Orcasite.Notifications.EmailTemplateTest do
use ExUnit.Case

describe "email templates" do
test "new_detection template exists and is readable" do
template_path = Path.expand("lib/orcasite/notifications/templates/new_detection.mjml.eex")
assert File.exists?(template_path), "new_detection template file should exist"

{:ok, content} = File.read(template_path)
assert String.length(content) > 0, "template should not be empty"
assert String.contains?(content, "<mjml>"), "template should contain MJML structure"
assert String.contains?(content, "A new detection has been submitted"), "template should contain expected content"
end

test "confirmed_candidate template exists and is readable" do
template_path = Path.expand("lib/orcasite/notifications/templates/confirmed_candidate.mjml.eex")
assert File.exists?(template_path), "confirmed_candidate template file should exist"

{:ok, content} = File.read(template_path)
assert String.length(content) > 0, "template should not be empty"
assert String.contains?(content, "<mjml>"), "template should contain MJML structure"
assert String.contains?(content, "LISTEN NOW!"), "template should contain expected content"
end

test "templates contain required variables" do
# Test new_detection template variables
{:ok, new_detection_content} = File.read(Path.expand("lib/orcasite/notifications/templates/new_detection.mjml.eex"))
assert String.contains?(new_detection_content, "{{ node_name }}"), "should contain node_name variable"
assert String.contains?(new_detection_content, "{{ node }}"), "should contain node variable"
assert String.contains?(new_detection_content, "meta[\"description\"]"), "should contain description variable"
assert String.contains?(new_detection_content, "{{unsubscribe_url}}"), "should contain unsubscribe_url variable"

# Test confirmed_candidate template variables
{:ok, confirmed_candidate_content} = File.read(Path.expand("lib/orcasite/notifications/templates/confirmed_candidate.mjml.eex"))
assert String.contains?(confirmed_candidate_content, "{{ node }}"), "should contain node variable"
assert String.contains?(confirmed_candidate_content, "meta[\"message\"]"), "should contain message variable"
assert String.contains?(confirmed_candidate_content, "{{ unsubscribe_url }}"), "should contain unsubscribe_url variable"
end
end
end