feat: Move mail system to shared
Server Deploy / deploy (push) Successful in 2m46s

This commit is contained in:
2026-07-28 09:50:46 +01:00
parent 5e326335af
commit a232c74990
49 changed files with 1206 additions and 706 deletions
@@ -0,0 +1,6 @@
{{define "pill"}}{{template "chip" (dict "label" "Cancelled" "tone" "pend")}}{{end}}
{{define "title"}}Your subscription is cancelled{{end}}
{{define "body"}}
{{template "lead" (printf "Your subscription for %s is cancelled." .InstanceName)}}
{{template "p" "Your instance keeps working until the current licence expires. After that, monitors keep running but changes are disabled."}}
{{end}}
+7
View File
@@ -0,0 +1,7 @@
{{define "subject"}}Your Vantage subscription is cancelled{{end}}
{{define "pill"}}{{template "chip" (dict "label" "Cancelled" "tone" "pend")}}{{end}}
{{define "title"}}Your subscription is cancelled{{end}}
{{define "body"}}
{{template "lead" (printf "Your subscription for %s is cancelled." .InstanceName)}}
{{template "p" "Your instance keeps working until the current licence expires. After that, monitors keep running but changes are disabled."}}
{{end}}
+11
View File
@@ -0,0 +1,11 @@
{{define "title"}}New contact enquiry{{end}}
{{define "body"}}
{{template "lead" "Someone has used the contact form on the Vantage site."}}
{{template "rows" (list
(dict "k" "Name" "v" .Name)
(dict "k" "Email" "v" .Email)
(dict "k" "Servers" "v" .Servers)
(dict "k" "Topic" "v" .Topic)
(dict "k" "Received" "v" .Received))}}
{{template "note" .Message}}
{{end}}
+13
View File
@@ -0,0 +1,13 @@
{{define "subject"}}[Vantage] {{.Topic}} {{.Email}}{{end}}
{{define "title"}}New contact enquiry{{end}}
{{define "body"}}
{{template "lead" "Someone has used the contact form on the Vantage site."}}
{{template "rows" (list
(dict "k" "Name" "v" .Name)
(dict "k" "Email" "v" .Email)
(dict "k" "Servers" "v" .Servers)
(dict "k" "Topic" "v" .Topic)
(dict "k" "Received" "v" .Received))}}
Message:
{{template "note" .Message}}
{{end}}
@@ -0,0 +1,7 @@
{{define "pill"}}{{template "chip" (dict "label" "Deletion scheduled" "tone" "down")}}{{end}}
{{define "title"}}{{.InstanceName}} will be deleted {{.When}}{{end}}
{{define "body"}}
{{template "lead" (printf "%s and everything in it will be deleted %s, on %s." .InstanceName .When (date .DeleteOn))}}
{{template "p" "This cannot be undone. Renew it to keep it:"}}
{{template "button" (dict "label" "Keep this instance" "url" .PortalURL)}}
{{end}}
@@ -0,0 +1,8 @@
{{define "subject"}}{{.InstanceName}} will be deleted {{.When}}{{end}}
{{define "pill"}}{{template "chip" (dict "label" "Deletion scheduled" "tone" "down")}}{{end}}
{{define "title"}}{{.InstanceName}} will be deleted {{.When}}{{end}}
{{define "body"}}
{{template "lead" (printf "%s and everything in it will be deleted %s, on %s." .InstanceName .When (date .DeleteOn))}}
{{template "p" "This cannot be undone. Renew it to keep it:"}}
{{template "button" (dict "label" "Keep this instance" "url" .PortalURL)}}
{{end}}
+8
View File
@@ -0,0 +1,8 @@
{{define "pill"}}{{template "chip" (dict "label" "Read-only" "tone" "down")}}{{end}}
{{define "title"}}{{.InstanceName}} is now read-only{{end}}
{{define "body"}}
{{template "lead" (printf "%s's Free licence has expired." .InstanceName)}}
{{template "p" "Your servers and monitors keep running and your agents keep their keys, but changes are disabled."}}
{{template "button" (dict "label" "Renew now" "url" .PortalURL)}}
{{if not .DeleteOn.IsZero}}{{template "p" (printf "If it is not renewed, the instance and everything in it will be deleted on %s." (date .DeleteOn))}}{{end}}
{{end}}
+9
View File
@@ -0,0 +1,9 @@
{{define "subject"}}{{.InstanceName}} is now read-only{{end}}
{{define "pill"}}{{template "chip" (dict "label" "Read-only" "tone" "down")}}{{end}}
{{define "title"}}{{.InstanceName}} is now read-only{{end}}
{{define "body"}}
{{template "lead" (printf "%s's Free licence has expired." .InstanceName)}}
{{template "p" "Your servers and monitors keep running and your agents keep their keys, but changes are disabled."}}
{{template "button" (dict "label" "Renew now" "url" .PortalURL)}}
{{if not .DeleteOn.IsZero}}{{template "p" (printf "If it is not renewed, the instance and everything in it will be deleted on %s." (date .DeleteOn))}}{{end}}
{{end}}
+7
View File
@@ -0,0 +1,7 @@
{{define "pill"}}{{template "chip" (dict "label" "Expiring soon" "tone" "pend")}}{{end}}
{{define "title"}}{{.InstanceName}} expires on {{shortDate .Expires}}{{end}}
{{define "body"}}
{{template "lead" (printf "%s's Free licence runs out on %s." .InstanceName (date .Expires))}}
{{template "button" (dict "label" "Renew in one click" "url" .PortalURL)}}
{{template "p" "If you do nothing, the instance keeps running but stops accepting changes."}}
{{end}}
+8
View File
@@ -0,0 +1,8 @@
{{define "subject"}}{{.InstanceName}} expires on {{shortDate .Expires}}{{end}}
{{define "pill"}}{{template "chip" (dict "label" "Expiring soon" "tone" "pend")}}{{end}}
{{define "title"}}{{.InstanceName}} expires on {{shortDate .Expires}}{{end}}
{{define "body"}}
{{template "lead" (printf "%s's Free licence runs out on %s." .InstanceName (date .Expires))}}
{{template "button" (dict "label" "Renew in one click" "url" .PortalURL)}}
{{template "p" "If you do nothing, the instance keeps running but stops accepting changes."}}
{{end}}
@@ -0,0 +1,8 @@
{{define "pill"}}{{template "chip" (dict "label" "Ready" "tone" "up")}}{{end}}
{{define "title"}}{{.InstanceName}} is ready{{end}}
{{define "body"}}
{{template "lead" (printf "%s is provisioned and waiting for you." .InstanceName)}}
{{if .LoginURL}}{{template "button" (dict "label" "Sign in" "url" .LoginURL)}}{{end}}
{{template "p" (printf "Your Free licence runs until %s. We will email you before then so you can renew it in one click." (date .Expires))}}
{{template "p" "Sign in with the same email address and password you use for your Vantage account. Changing your Vantage HQ password changes it here too."}}
{{end}}
@@ -0,0 +1,9 @@
{{define "subject"}}{{.InstanceName}} is ready{{end}}
{{define "pill"}}{{template "chip" (dict "label" "Ready" "tone" "up")}}{{end}}
{{define "title"}}{{.InstanceName}} is ready{{end}}
{{define "body"}}
{{template "lead" (printf "%s is provisioned and waiting for you." .InstanceName)}}
{{if .LoginURL}}{{template "button" (dict "label" "Sign in" "url" .LoginURL)}}{{end}}
{{template "p" (printf "Your Free licence runs until %s. We will email you before then so you can renew it in one click." (date .Expires))}}
{{template "p" "Sign in with the same email address and password you use for your Vantage account. Changing your Vantage HQ password changes it here too."}}
{{end}}
+7
View File
@@ -0,0 +1,7 @@
{{define "title"}}You have been invited to {{.AccountName}}{{end}}
{{define "body"}}
{{template "lead" (printf "You have been invited to join %s on Vantage." .AccountName)}}
{{template "p" "Set your own password and finish joining:"}}
{{template "button" (dict "label" "Set password and join" "url" .Link)}}
{{template "p" "This link expires in 24 hours. If you were not expecting this, ignore it — nothing happens until you open the link."}}
{{end}}
+8
View File
@@ -0,0 +1,8 @@
{{define "subject"}}You have been invited to {{.AccountName}} on Vantage{{end}}
{{define "title"}}You have been invited to {{.AccountName}}{{end}}
{{define "body"}}
{{template "lead" (printf "You have been invited to join %s on Vantage." .AccountName)}}
{{template "p" "Set your own password and finish joining:"}}
{{template "button" (dict "label" "Set password and join" "url" .Link)}}
{{template "p" "This link expires in 24 hours. If you were not expecting this, ignore it — nothing happens until you open the link."}}
{{end}}
+121
View File
@@ -0,0 +1,121 @@
{{- /*
The Vantage email shell.
Every colour in the email system lives in this file and nowhere else, in the
same way no component in web/, site/ or adminsite/ carries a hex. The values
are web/app/globals.css's tokens — an email is read before the recipient
clicks through to the control plane, so the two should not look like
different products. They are written as literal hex here because email
clients support neither var() nor a reliable prefers-color-scheme, so the
usual token indirection is not available: when you change a token in the
three globals.css files, change it here too.
--ground #071628 --rule #1e3855
--panel #0d2138 --accent #5b9be8
--panel-2 #102842 --up #4fb484
--ink #e4ecf6 --down #e2705a
--ink-2 #9fb3ca --pend #d6a63f
--ink-3 #71879f --well #04101f
Layout is tables and inline styles throughout, which is not a stylistic
choice — it is the only thing Outlook renders predictably.
A message file overrides "title", "pill" and "body"; the empty defaults below
exist so that a message needing no pill does not have to define one.
*/ -}}
{{- define "title"}}{{end -}}
{{- define "pill"}}{{end -}}
{{- define "body"}}{{end -}}
{{- /* p renders one paragraph of body copy. */ -}}
{{- define "p" -}}
<p style="margin:0 0 16px;color:#9fb3ca;font-size:14px;line-height:1.6;">{{.}}</p>
{{- end -}}
{{- /* lead is the first paragraph: same size, brighter, sets the subject. */ -}}
{{- define "lead" -}}
<p style="margin:0 0 16px;color:#e4ecf6;font-size:15px;line-height:1.6;">{{.}}</p>
{{- end -}}
{{- /* button takes dict "label" "…" "url" "…".
The bare URL is printed underneath on purpose: a plain-text-preferring
client, a stripped-styles inbox and a forwarded message all lose the
anchor, and a verification email whose link cannot be reached is a
support ticket. */ -}}
{{- define "button" -}}
<table role="presentation" cellpadding="0" cellspacing="0" style="margin:4px 0 16px;">
<tr>
<td style="border-radius:4px;background:#5b9be8;">
<a href="{{.url}}" style="display:inline-block;padding:10px 20px;color:#04101f;font-size:14px;font-weight:600;text-decoration:none;">{{.label}}</a>
</td>
</tr>
</table>
<p style="margin:0 0 20px;color:#71879f;font-size:12px;line-height:1.5;word-break:break-all;">
Or paste this into your browser:<br>
<a href="{{.url}}" style="color:#5b9be8;text-decoration:none;">{{.url}}</a>
</p>
{{- end -}}
{{- /* well shows machine output — a licence blob, an install ID. Mirrors
web/'s --well surface, the floor beneath the ground. */ -}}
{{- define "well" -}}
<pre style="margin:0 0 20px;padding:14px;background:#04101f;border:1px solid #1e3855;border-radius:4px;color:#9fb3ca;font-family:ui-monospace,'Cascadia Mono','SF Mono',Menlo,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-all;">{{.}}</pre>
{{- end -}}
{{- /* note is a quoted callout, used for a free-text message we did not
write ourselves. */ -}}
{{- define "note" -}}
<p style="margin:0 0 20px;padding:12px 14px;background:#102842;border:1px solid #1e3855;border-radius:4px;color:#e4ecf6;font-size:13px;line-height:1.6;">{{.}}</p>
{{- end -}}
{{- /* rows takes a list of dict "k" "…" "v" "…". */ -}}
{{- define "rows" -}}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="margin:4px 0 8px;border-top:1px solid #1e3855;">
{{- range .}}
<tr>
<td style="padding:9px 0;border-bottom:1px solid #172c44;color:#71879f;font-size:13px;width:130px;vertical-align:top;">{{.k}}</td>
<td style="padding:9px 0;border-bottom:1px solid #172c44;color:#e4ecf6;font-size:13px;font-weight:500;">{{.v}}</td>
</tr>
{{- end}}
</table>
{{- end -}}
{{- /* chip takes dict "label" "…" "tone" "up|down|pend|accent". Tone is
never the only signal: the label spells the state out. */ -}}
{{- define "chip" -}}
{{- $fg := "#5b9be8"}}{{if eq .tone "up"}}{{$fg = "#4fb484"}}{{else if eq .tone "down"}}{{$fg = "#e2705a"}}{{else if eq .tone "pend"}}{{$fg = "#d6a63f"}}{{end -}}
<span style="display:inline-block;margin:0 0 12px;padding:4px 11px;border:1px solid {{$fg}};border-radius:9999px;color:{{$fg}};font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;">{{.label}}</span>
{{- end -}}
<!DOCTYPE html>
<html>
<body style="margin:0;padding:0;background:#071628;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#071628;padding:32px 12px;">
<tr>
<td align="center">
<table role="presentation" width="520" cellpadding="0" cellspacing="0" style="max-width:520px;width:100%;background:#0d2138;border:1px solid #1e3855;border-radius:4px;overflow:hidden;font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;">
<tr><td style="height:3px;background:#5b9be8;"></td></tr>
<tr>
<td style="padding:26px 28px 8px;">
<span style="font-size:16px;font-weight:700;letter-spacing:-.01em;color:#7fb2f0;">Vantage</span>
</td>
</tr>
<tr>
<td style="padding:10px 28px 26px;">
{{template "pill" .}}
<h1 style="margin:2px 0 14px;font-size:20px;font-weight:700;line-height:1.3;color:#e4ecf6;">{{template "title" .}}</h1>
{{template "body" .}}
</td>
</tr>
<tr>
<td style="padding:14px 28px;border-top:1px solid #1e3855;background:#04101f;">
<p style="margin:0;color:#71879f;font-size:12px;line-height:1.5;">Sent by Vantage · infrastructure control plane</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
+46
View File
@@ -0,0 +1,46 @@
{{- /*
The plain-text counterpart of layout.html.tmpl.
It defines the same helper names — p, lead, button, well, note, rows, chip —
so a message's txt file reads as the same document as its html one, and a
helper added on one side is obvious by its absence on the other.
"subject" is defined here rather than in the HTML file: html/template would
escape an ampersand in an instance name, and mail clients show subject lines
verbatim.
*/ -}}
{{- define "subject"}}Vantage{{end -}}
{{- define "title"}}{{end -}}
{{- define "pill"}}{{end -}}
{{- define "body"}}{{end -}}
{{- define "p"}}{{.}}
{{end -}}
{{- define "lead"}}{{.}}
{{end -}}
{{- define "button"}}{{.label}}:
{{.url}}
{{end -}}
{{- define "well"}}{{.}}
{{end -}}
{{- define "note"}}{{.}}
{{end -}}
{{- define "rows"}}{{range .}}{{.k}}: {{.v}}
{{end}}
{{end -}}
{{- define "chip"}}[{{upper .label}}]
{{end -}}
VANTAGE
{{template "pill" .}}
{{template "title" .}}
{{template "body" .}}
--
Sent by Vantage · infrastructure control plane
+7
View File
@@ -0,0 +1,7 @@
{{define "title"}}Your licence key{{end}}
{{define "body"}}
{{template "lead" (printf "Your licence for %s is below." .InstanceName)}}
{{template "p" "Paste it into Settings → Licence on your Vantage install:"}}
{{template "well" .Blob}}
{{template "p" "The licence is signed public data, not a secret — it is useless on any instance other than the one it names."}}
{{end}}
+8
View File
@@ -0,0 +1,8 @@
{{define "subject"}}Your Vantage licence key{{end}}
{{define "title"}}Your licence key{{end}}
{{define "body"}}
{{template "lead" (printf "Your licence for %s is below." .InstanceName)}}
{{template "p" "Paste it into Settings > Licence on your Vantage install:"}}
{{template "well" .Blob}}
{{template "p" "The licence is signed public data, not a secret — it is useless on any instance other than the one it names."}}
{{end}}
@@ -0,0 +1,7 @@
{{define "pill"}}{{template "chip" (dict "label" "Action needed" "tone" "pend")}}{{end}}
{{define "title"}}Finish setting up {{.InstanceName}}{{end}}
{{define "body"}}
{{template "lead" (printf "Your subscription for %s is active, but the instance is not linked yet." .InstanceName)}}
{{template "p" "Paste your install's ID in the portal to receive your licence."}}
{{if .PortalURL}}{{template "button" (dict "label" "Link my install" "url" .PortalURL)}}{{end}}
{{end}}
@@ -0,0 +1,8 @@
{{define "subject"}}Finish setting up {{.InstanceName}}{{end}}
{{define "pill"}}{{template "chip" (dict "label" "Action needed" "tone" "pend")}}{{end}}
{{define "title"}}Finish setting up {{.InstanceName}}{{end}}
{{define "body"}}
{{template "lead" (printf "Your subscription for %s is active, but the instance is not linked yet." .InstanceName)}}
{{template "p" "Paste your install's ID in the portal to receive your licence."}}
{{if .PortalURL}}{{template "button" (dict "label" "Link my install" "url" .PortalURL)}}{{end}}
{{end}}
@@ -0,0 +1,10 @@
{{define "pill"}}{{if .Down}}{{template "chip" (dict "label" "Down" "tone" "down")}}{{else}}{{template "chip" (dict "label" "Recovered" "tone" "up")}}{{end}}{{end}}
{{define "title"}}{{.MonitorName}}{{end}}
{{define "body"}}
{{template "p" (printf "%s check" .Type)}}
{{if .Message}}{{template "note" .Message}}{{end}}
{{template "rows" (list
(dict "k" "Status" "v" (printf "%s → %s" .OldStatus .NewStatus))
(dict "k" "Type" "v" .Type)
(dict "k" "Time" "v" (stamp .Time)))}}
{{end}}
@@ -0,0 +1,11 @@
{{define "subject"}}[Vantage] {{.MonitorName}} ({{.Type}}) {{if .Down}}is DOWN{{else}}recovered{{end}}{{if .Message}}: {{.Message}}{{end}}{{end}}
{{define "pill"}}{{if .Down}}{{template "chip" (dict "label" "Down" "tone" "down")}}{{else}}{{template "chip" (dict "label" "Recovered" "tone" "up")}}{{end}}{{end}}
{{define "title"}}{{.MonitorName}}{{end}}
{{define "body"}}
{{template "p" (printf "%s check" .Type)}}
{{if .Message}}{{template "note" .Message}}{{end}}
{{template "rows" (list
(dict "k" "Status" "v" (printf "%s -> %s" .OldStatus .NewStatus))
(dict "k" "Type" "v" .Type)
(dict "k" "Time" "v" (stamp .Time)))}}
{{end}}
+7
View File
@@ -0,0 +1,7 @@
{{define "pill"}}{{template "chip" (dict "label" "Payment failed" "tone" "pend")}}{{end}}
{{define "title"}}Payment failed for {{.InstanceName}}{{end}}
{{define "body"}}
{{template "lead" (printf "A payment for %s failed." .InstanceName)}}
{{template "p" "Your instance is unaffected while the card is retried. Update your payment method from the billing portal."}}
{{if .PortalURL}}{{template "button" (dict "label" "Open billing portal" "url" .PortalURL)}}{{end}}
{{end}}
+8
View File
@@ -0,0 +1,8 @@
{{define "subject"}}Payment failed for your Vantage subscription{{end}}
{{define "pill"}}{{template "chip" (dict "label" "Payment failed" "tone" "pend")}}{{end}}
{{define "title"}}Payment failed for {{.InstanceName}}{{end}}
{{define "body"}}
{{template "lead" (printf "A payment for %s failed." .InstanceName)}}
{{template "p" "Your instance is unaffected while the card is retried. Update your payment method from the billing portal."}}
{{if .PortalURL}}{{template "button" (dict "label" "Open billing portal" "url" .PortalURL)}}{{end}}
{{end}}
+6
View File
@@ -0,0 +1,6 @@
{{define "pill"}}{{template "chip" (dict "label" "Renewed" "tone" "up")}}{{end}}
{{define "title"}}{{.InstanceName}} is renewed{{end}}
{{define "body"}}
{{template "lead" (printf "Your Free licence for %s now runs until %s." .InstanceName (date .Expires))}}
{{template "p" "Nothing else changes — your servers, agents and monitors carry on as they were."}}
{{end}}
+7
View File
@@ -0,0 +1,7 @@
{{define "subject"}}{{.InstanceName}} renewed{{end}}
{{define "pill"}}{{template "chip" (dict "label" "Renewed" "tone" "up")}}{{end}}
{{define "title"}}{{.InstanceName}} is renewed{{end}}
{{define "body"}}
{{template "lead" (printf "Your Free licence for %s now runs until %s." .InstanceName (date .Expires))}}
{{template "p" "Nothing else changes — your servers, agents and monitors carry on as they were."}}
{{end}}
@@ -0,0 +1,7 @@
{{define "title"}}Confirm your email address{{end}}
{{define "body"}}
{{template "lead" "Confirm this address to finish setting up your Vantage account."}}
{{template "button" (dict "label" "Confirm email address" "url" .Link)}}
{{template "p" (printf "The link works once and expires in %d hours." .TTLHours)}}
{{template "p" "If you did not request this, ignore this email — nothing happens until the link is opened."}}
{{end}}
@@ -0,0 +1,8 @@
{{define "subject"}}Verify your Vantage account{{end}}
{{define "title"}}Confirm your email address{{end}}
{{define "body"}}
{{template "lead" "Confirm this address to finish setting up your Vantage account."}}
{{template "button" (dict "label" "Confirm email address" "url" .Link)}}
{{template "p" (printf "The link works once and expires in %d hours." .TTLHours)}}
{{template "p" "If you did not request this, ignore this email — nothing happens until the link is opened."}}
{{end}}