From 21efa96ca28c715a156dc648fbbfecf42f759f76 Mon Sep 17 00:00:00 2001 From: Austin French Date: Mon, 16 Mar 2026 21:04:52 -0700 Subject: [PATCH 1/2] Add briefing email API for Hermes cron digests - BriefingMailer with dark-themed HTML template - POST /api/v1/briefings/send endpoint with Bearer auth - Accepts dynamic sections (title + HTML content) - Plain text fallback included --- .../api/v1/briefings_controller.rb | 44 +++++++++++++++ app/mailers/briefing_mailer.rb | 8 +++ app/views/briefing_mailer/digest.html.erb | 56 +++++++++++++++++++ app/views/briefing_mailer/digest.text.erb | 16 ++++++ config/routes.rb | 3 + 5 files changed, 127 insertions(+) create mode 100644 app/controllers/api/v1/briefings_controller.rb create mode 100644 app/mailers/briefing_mailer.rb create mode 100644 app/views/briefing_mailer/digest.html.erb create mode 100644 app/views/briefing_mailer/digest.text.erb diff --git a/app/controllers/api/v1/briefings_controller.rb b/app/controllers/api/v1/briefings_controller.rb new file mode 100644 index 0000000..05ad900 --- /dev/null +++ b/app/controllers/api/v1/briefings_controller.rb @@ -0,0 +1,44 @@ +module Api + module V1 + class BriefingsController < ApplicationController + skip_before_action :verify_authenticity_token + before_action :authenticate_api_key + + def send_briefing + recipient = params[:recipient] + subject = params[:subject] + sections = params[:sections] + + if recipient.blank? || subject.blank? || sections.blank? + return render json: { error: "Missing required fields: recipient, subject, sections" }, status: :unprocessable_entity + end + + BriefingMailer.digest( + recipient: recipient, + subject: subject, + sections: sections.map(&:to_unsafe_h) + ).deliver_later + + render json: { success: true, message: "Briefing email queued for delivery to #{recipient}" } + rescue StandardError => e + render json: { error: e.message }, status: :internal_server_error + end + + private + + def authenticate_api_key + api_key = ENV["BRIEFING_API_KEY"] + token = request.headers["Authorization"]&.remove("Bearer ") + + if api_key.blank? + render json: { error: "BRIEFING_API_KEY not configured" }, status: :service_unavailable + return + end + + unless ActiveSupport::SecurityUtils.secure_compare(token.to_s, api_key) + render json: { error: "Unauthorized" }, status: :unauthorized + end + end + end + end +end diff --git a/app/mailers/briefing_mailer.rb b/app/mailers/briefing_mailer.rb new file mode 100644 index 0000000..b26f153 --- /dev/null +++ b/app/mailers/briefing_mailer.rb @@ -0,0 +1,8 @@ +class BriefingMailer < ApplicationMailer + def digest(recipient:, subject:, sections:) + @sections = sections + @subject = subject + + mail(to: recipient, subject: subject) + end +end diff --git a/app/views/briefing_mailer/digest.html.erb b/app/views/briefing_mailer/digest.html.erb new file mode 100644 index 0000000..e1bfb14 --- /dev/null +++ b/app/views/briefing_mailer/digest.html.erb @@ -0,0 +1,56 @@ + + + + + + <%= @subject %> + + + + + + +
+ + + + + + + + <% @sections.each_with_index do |section, index| %> + + + + <% end %> + + + + + +
+

<%= @subject %>

+
+
+ + + + + + + + + +
+

<%= section[:title] || section["title"] %>

+
+ <%= raw(section[:content] || section["content"]) %> +
+
+

+ Sent by Hermes Agent from austn.net +

+
+
+ + diff --git a/app/views/briefing_mailer/digest.text.erb b/app/views/briefing_mailer/digest.text.erb new file mode 100644 index 0000000..7de0031 --- /dev/null +++ b/app/views/briefing_mailer/digest.text.erb @@ -0,0 +1,16 @@ +<%= @subject %> +<%= "=" * @subject.length %> + +<% @sections.each_with_index do |section, index| %> +<%= (section[:title] || section["title"]).upcase %> +<%= "-" * (section[:title] || section["title"]).length %> + +<%= strip_tags(section[:content] || section["content"]) %> + +<% unless index == @sections.length - 1 %> +--- + +<% end %> +<% end %> +-- +Sent by Hermes Agent from austn.net diff --git a/config/routes.rb b/config/routes.rb index 8f03b4f..eb29dc0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -229,6 +229,9 @@ post "images/generate", to: "images#generate" # Sync - blocks until complete post "images/generate_async", to: "images#generate_async" # Async - returns generation ID get "images/:id/status", to: "images#status", as: :image_status + + # Briefing email endpoint + post "briefings/send", to: "briefings#send_briefing" end end From 843f2cad4f1c23077d1d1c8487e4e2685c71fe4f Mon Sep 17 00:00:00 2001 From: Austin French Date: Mon, 16 Mar 2026 21:06:44 -0700 Subject: [PATCH 2/2] Simplify briefing controller - hardcode recipient, remove auth This is an internal-only endpoint sending to a fixed address, no need for API key auth. --- .../api/v1/briefings_controller.rb | 28 ++++--------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/app/controllers/api/v1/briefings_controller.rb b/app/controllers/api/v1/briefings_controller.rb index 05ad900..70fe429 100644 --- a/app/controllers/api/v1/briefings_controller.rb +++ b/app/controllers/api/v1/briefings_controller.rb @@ -2,43 +2,27 @@ module Api module V1 class BriefingsController < ApplicationController skip_before_action :verify_authenticity_token - before_action :authenticate_api_key + + RECIPIENT = "austindanielfrench@gmail.com".freeze def send_briefing - recipient = params[:recipient] subject = params[:subject] sections = params[:sections] - if recipient.blank? || subject.blank? || sections.blank? - return render json: { error: "Missing required fields: recipient, subject, sections" }, status: :unprocessable_entity + if subject.blank? || sections.blank? + return render json: { error: "Missing required fields: subject, sections" }, status: :unprocessable_entity end BriefingMailer.digest( - recipient: recipient, + recipient: RECIPIENT, subject: subject, sections: sections.map(&:to_unsafe_h) ).deliver_later - render json: { success: true, message: "Briefing email queued for delivery to #{recipient}" } + render json: { success: true, message: "Briefing email queued for delivery" } rescue StandardError => e render json: { error: e.message }, status: :internal_server_error end - - private - - def authenticate_api_key - api_key = ENV["BRIEFING_API_KEY"] - token = request.headers["Authorization"]&.remove("Bearer ") - - if api_key.blank? - render json: { error: "BRIEFING_API_KEY not configured" }, status: :service_unavailable - return - end - - unless ActiveSupport::SecurityUtils.secure_compare(token.to_s, api_key) - render json: { error: "Unauthorized" }, status: :unauthorized - end - end end end end