Files
warmbly/internal/notify/templates/reset_password.go
T
Matthew Meszaros f04bd26b44 feat: comprehensive unibox + WS latency + dashboard-style transactional emails
User: "inbox is really really bad. So I want all possible ways to
search for an email that we can do... realtime for everything and the
dashboard to show our latency... show how much unread emails." Plus a
follow-up: "I don't like how the emails looks like because they have
that blue gradient, I want dashboard style good one."

Inbox:
- Wired the backend search endpoint (GET /unibox with from/subject/
  unseen/since/until/cursor/limit) — was implemented server-side but
  the frontend was never calling it. Inbox now actually reflects
  server data.
- New UniboxSearchParams model + searchIncoming client + infinite
  useUniboxSearch hook that drops null rows defensively.
- ConversationList: SearchInput (subject substring) + quick-filter
  strip (All / Unread / Today / This week). Unread count surfaces
  in the SectionBar header AND on the Unread chip. Skeleton +
  explicit error block with retry; "Load more · N shown" when more
  pages are available.
- UniboxFilterSheet (advanced filters in the right-side panel):
  free-text query, sender substring, account picker pulled from
  the user's connected mailboxes, status toggle (Any/Unread/Read),
  since/until date pickers with toggle, newest/oldest sort. Draft
  state mirrors parent until Apply.

LivePanel telemetry (sidebar):
- Real WS roundtrip latency. SocketProvider stamps performance.now()
  per heartbeat ref; phoenix phx_reply with that ref computes the
  delta and publishes via setWsLatencyMs. LivePanel colour-codes
  the latency text: <100ms emerald, <300ms amber, ≥300ms red, "—"
  when disconnected.
- Unread count row reads from useAppStore.unseenCount.
- Status label: OFFLINE / CONNECTING / LIVE (with pulse) / IDLE,
  tied to connectionStatus + active mailbox count.

Transactional emails (no more blue gradient):
- base.go rewritten as dashboard chrome: cream #f5f6f8 background,
  white card with hairline #e2e8f0 border, 8px radius, slate-900
  text. Logo monogram in slate, no decorative haze, no gradients.
- login_code / registration_code: tiny uppercase eyebrow + 18px
  bold heading + neutral body + monospace code pill in a hairline-
  bordered box. No serif type.
- reset_password / welcome: same chrome. Slate-900 primary button
  replaces the sky-gradient one. Plaintext link below for accessible
  fallback.
- Template tests updated against the new markup; all green.
2026-05-23 10:07:11 +00:00

50 lines
2.3 KiB
Go

package templates
import (
"bytes"
"html/template"
"github.com/getsentry/sentry-go"
)
const resetPasswordContent = `
<p style="margin:0 0 4px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;font-size:11px;color:#94a3b8;letter-spacing:0.14em;text-transform:uppercase;font-weight:500;">
Account
</p>
<h2 style="margin:0 0 8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;font-weight:600;font-size:18px;color:#0f172a;letter-spacing:-0.01em;">
Reset your password
</h2>
<p style="margin:0 0 24px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;font-size:13px;color:#475569;line-height:20px;">
We received a request to reset your password. Click the button to choose a new one. If you didn't request this, you can safely ignore this email.
</p>
<table cellpadding="0" cellspacing="0" border="0" align="center" role="presentation" style="margin:0 0 24px;">
<tr>
<td align="center" style="border-radius:6px;background:#0f172a;">
<a href="{{.ResetURL}}" target="_blank" style="display:inline-block;padding:10px 22px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;font-size:13px;font-weight:600;color:#ffffff;text-decoration:none;letter-spacing:0.01em;">Reset password</a>
</td>
</tr>
</table>
<div style="margin:0 0 16px;height:1px;background:#e2e8f0;"></div>
<p style="margin:0 0 6px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;font-size:11px;color:#94a3b8;letter-spacing:0.08em;text-transform:uppercase;font-weight:500;">
Link expires in 4 hours
</p>
<p style="margin:0;font-family:'SF Mono','Fira Mono','Roboto Mono','Courier New',monospace;font-size:11px;color:#64748b;word-break:break-all;line-height:18px;">
<a href="{{.ResetURL}}" style="color:#0f172a;text-decoration:none;">{{.ResetURL}}</a>
</p>
`
var resetPasswordTmpl = template.Must(template.New("reset_password_content").Parse(resetPasswordContent))
func GenerateResetPasswordHTML(firstName, url string) (string, error) {
data := struct{ ResetURL string }{ResetURL: url}
var buf bytes.Buffer
if err := resetPasswordTmpl.Execute(&buf, data); err != nil {
sentry.CaptureException(err)
return "", err
}
return renderEmail("Reset Your Password", buf.String())
}