Add mode 1 mailer for testing.

This commit is contained in:
Eli Ribble 2026-03-31 17:34:08 +00:00
parent 7b3c1f2b54
commit 4f96f35d9a
No known key found for this signature in database
8 changed files with 258 additions and 14 deletions

View file

@ -7,7 +7,6 @@
<!-- Bootstrap CSS -->
<link href="/static/vendor/css/bootstrap.min.css" rel="stylesheet" />
<!-- Bootstrap Icons -->
<link href="/static/css/bootstrap.css" rel="stylesheet" />
<link rel="icon" href="/static/ico/favicon-sync.ico" type="image/x-icon" />
{{ block "extraheader" . }}{{ end }}
{{ if not .Config.IsProductionEnvironment }}
@ -17,6 +16,6 @@
<body>
{{ template "content" . }}
<div id="flogo"></div>
<script src="/static/vendor/js/bootstrap.bundle.min.js"></script>
<script src="/static/vendor/bootstrap-5.3.8/bootstrap.bundle.min.js"></script>
</body>
</html>

View file

@ -0,0 +1,176 @@
{{ template "sync/layout/base.html" . }}
{{ define "title" }}Mailer{{ end }}
{{ define "extraheader" }}
<style>
@media print {
@page {
margin: 0.25in;
}
}
body {
margin: 0.25in;
font-family: "Times New Roman", serif;
}
.address-space {
position: absolute;
top: 0.2in;
left: 0.5in;
width: 3.25in;
height: 2.6in;
/* Uncomment below to visualize the address space during development */
/*border: 1px dashed #ccc;*/
}
.header-section {
margin-top: 2.8in; /* Clear the address space */
margin-bottom: 0.5in;
}
.pool-image {
float: right;
width: 2.5in;
height: 2in;
margin-left: 0.25in;
margin-bottom: 0.25in;
/*border: 2px solid #0dcaf0;
background-color: #e7f6ff;*/
display: flex;
align-items: center;
justify-content: center;
color: #0dcaf0;
font-style: italic;
}
.letter-content {
text-align: justify;
line-height: 1.6;
}
.logo-space {
text-align: center;
margin-top: 1in;
margin-bottom: 0.5in;
}
.logo {
width: 1.5in;
height: 1.5in;
display: inline-flex;
align-items: center;
justify-content: center;
color: #198754;
font-style: italic;
}
.signature-line {
margin-top: 0.75in;
margin-left: 3in;
}
h2 {
color: #0d6efd;
margin-bottom: 0.5in;
}
.date {
margin-bottom: 0.3in;
}
</style>
{{ end }}
{{ define "content" }}
<!-- Address Space (will be populated automatically) -->
<div class="address-space"></div>
<!-- Main Letter Content -->
<div class="header-section">
<!-- Pool Image Placeholder -->
<img class="pool-image" src="/mailer/pool/random" />
<div class="date"><strong>Date:</strong>March 31, 2026</div>
</div>
<div class="letter-content">
<p>Dear Valued Member of Humanity,</p>
<p>
It has come to our attention that you are currently alive.
Congratulations! However, we must inform you that this status is under
constant threat by flying vampires no bigger than your pinky fingernail.
We are, of course, referring to <strong>mosquitoes</strong>—nature's tiny
terrorists.
</p>
<p>
Mosquitoes are responsible for spreading diseases like malaria, dengue
fever, West Nile virus, Zika, and a whole host of other ailments that
sound like rejected Harry Potter spells. "Dengue Fever-osa!" just doesn't
have the same ring to it, but it's certainly more dangerous than turning
your teacup into a toad.
</p>
<p>
The image you see to the right is an actual aerial photograph taken from a
plane of a location you probably don't care about. It's there so we can
see how good the print quality is. Hopefully it's good, but we won't know
until you bring it to us. Please
<b>take this letter to Benjamin Sperry.</b>
</p>
<p>
<strong>Why should you care?</strong> Because these miniature menaces are
plotting against you RIGHT NOW. While you're reading this letter,
mosquitoes are gathering in small committees near standing water, planning
their next attack. They don't sleep. They don't rest. They simply exist to
ruin your backyard barbecues and transmit diseases.
</p>
<p>
The good news? Delta MVCD is like the Avengers, but for bugs. We're out
there draining standing water, applying treatments, and generally making
life difficult for mosquitoes everywhere. We're the heroes you didn't know
you needed, fighting the villain too small to see in the dark.
</p>
<p><strong>What can YOU do?</strong></p>
<ul>
<li>
Eliminate standing water around your property (mosquitoes can breed in a
bottle cap—yes, really)
</li>
<li>
Wear EPA-approved insect repellent (become unfashionable to mosquitoes)
</li>
<li>
Install or repair window screens (the medieval castle defense strategy,
but modernized)
</li>
<li>
Support your local mosquito control district (that's us—we accept praise
and appreciation)
</li>
</ul>
<p>
Remember: An ounce of prevention is worth a pound of calamine lotion and
several days of uncontrollable itching. Together, we can make our
community less hospitable to these winged hypodermic needles and more
enjoyable for actual humans.
</p>
<p>Stay vigilant. Stay bite-free. Stay alive.</p>
<div class="signature-line">
<p><strong>Sincerely,</strong></p>
<p>Eli Ribble</p>
<p>Founder of Gleipnir</p>
</div>
</div>
<!-- Company Logo Space -->
<div class="logo-space">
<img class="logo" src="/static/img/nidus-logo-256-transparent.png" />
</div>
{{ end }}

View file

@ -10,14 +10,14 @@ import (
"github.com/rs/zerolog/log"
)
func GeneratePDF(ctx context.Context, code string) ([]byte, error) {
func GeneratePDF(ctx context.Context, path string) ([]byte, error) {
// create context
chrome_ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
// capture pdf
var buf []byte
url := fmt.Sprintf("http://%s/mailer/%s/preview", config.Bind, code)
url := fmt.Sprintf("http://%s%s", config.Bind, path)
log.Info().Str("url", url).Msg("Getting with headless chrome")
if err := chromedp.Run(chrome_ctx, printToPDF(url, &buf)); err != nil {
return nil, fmt.Errorf("print to pdf: %w", err)

View file

@ -6,6 +6,7 @@ import (
"embed"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"path/filepath"
@ -14,7 +15,8 @@ import (
"github.com/Gleipnir-Technology/arcgis-go/fieldseeker"
"github.com/aarondl/opt/omit"
//"github.com/Gleipnir-Technology/bob"
//"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql"
"github.com/Gleipnir-Technology/bob/dialect/psql/sm"
"github.com/Gleipnir-Technology/nidus-sync/config"
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
@ -97,6 +99,30 @@ func ImageAtPoint(ctx context.Context, org Organization, level uint, lat, lng fl
IsPlaceholder: false,
}, nil
}
// Writes a random tile from the cache. This is a very odd thing to do, it's for testing
func WriteTileRandom(ctx context.Context, w http.ResponseWriter) error {
tile_rows, err := models.TileCachedImages.Query(
sm.Where(psql.Quote("is_empty").EQ(psql.Arg(false))),
sm.Limit(100),
).All(ctx, db.PGInstance.BobDB)
if err != nil {
return fmt.Errorf("get tiles: %w", err)
}
tile_row := tile_rows[rand.Intn(len(tile_rows))]
tile_path := tilePath(tile_row.ArcgisID, uint(tile_row.Z), uint(tile_row.Y), uint(tile_row.X))
var tile *TileRaster
if tile_row.IsEmpty {
tile = TileRasterPlaceholder()
} else {
tile, err = loadTileFromDisk(tile_path)
if err != nil {
return fmt.Errorf("load tile from disk: %w", err)
}
}
log.Debug().Int32("z", tile_row.Z).Int32("y", tile_row.Y).Int32("x", tile_row.X).Bool("is empty", tile_row.IsEmpty).Msg("random tile")
return writeTile(w, tile)
}
func loadTileFromDisk(tile_path string) (*TileRaster, error) {
file, err := os.Open(tile_path)
if err != nil {

View file

@ -91,6 +91,7 @@ func CreateUser(ctx context.Context, username string, name string, password_hash
log.Info().Int32("id", o.ID).Msg("Created organization")
u_setter := models.UserSetter{
DisplayName: omit.From(name),
IsActive: omit.From(true),
OrganizationID: omit.From(o.ID),
PasswordHash: omit.From(password_hash),
PasswordHashType: omit.From(enums.HashtypeBcrypt14),

View file

@ -10,9 +10,11 @@ import (
"github.com/Gleipnir-Technology/nidus-sync/db"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/html"
"github.com/Gleipnir-Technology/nidus-sync/platform"
"github.com/Gleipnir-Technology/nidus-sync/platform/pdf"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
)
type contentMailer struct {
@ -25,7 +27,22 @@ type contentMailer struct {
ReportURL string
}
func getMailer(w http.ResponseWriter, r *http.Request) {
func getMailer1(w http.ResponseWriter, r *http.Request) {
path := "/mailer/mode-1/preview"
content, err := pdf.GeneratePDF(r.Context(), path)
if err != nil {
respondError(w, "generate pdf failure", err, http.StatusInternalServerError)
return
}
err = writePDF(w, content, "mailer-mode-1.pdf")
if err != nil {
respondError(w, "copy error", err, http.StatusInternalServerError)
return
}
}
func getMailer2(w http.ResponseWriter, r *http.Request) {
}
func getMailer3(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
if code == "" {
http.Error(w, "empty code", http.StatusBadRequest)
@ -37,16 +54,27 @@ func getMailer(w http.ResponseWriter, r *http.Request) {
respondError(w, "generate pdf failure", err, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/pdf")
disposition := fmt.Sprintf("attachment; filename=\"compliance-mailer-%s.pdf\"", code)
w.Header().Set("Content-Disposition", disposition)
_, err = io.Copy(w, bytes.NewReader(content))
filename := fmt.Sprintf("compliance-mailer-%s.pdf", code)
err = writePDF(w, content, filename)
if err != nil {
respondError(w, "copy error", err, http.StatusInternalServerError)
return
}
}
func getMailerPreview(w http.ResponseWriter, r *http.Request) {
func writePDF(w http.ResponseWriter, content []byte, filename string) error {
w.Header().Set("Content-Type", "application/pdf")
disposition := fmt.Sprintf("attachment; filename=\"%s\"", filename)
w.Header().Set("Content-Disposition", disposition)
_, err := io.Copy(w, bytes.NewReader(content))
return err
}
func getMailer1Preview(w http.ResponseWriter, r *http.Request) {
html.RenderOrError(w, "sync/mailer-1.html", contentMailer{})
}
func getMailer2Preview(w http.ResponseWriter, r *http.Request) {
html.RenderOrError(w, "sync/mailer-2.html", contentMailer{})
}
func getMailer3Preview(w http.ResponseWriter, r *http.Request) {
code := chi.URLParam(r, "code")
if code == "" {
http.Error(w, "empty code", http.StatusBadRequest)
@ -70,7 +98,7 @@ func getMailerPreview(w http.ResponseWriter, r *http.Request) {
return
}
doc_id := uuid.New()
html.RenderOrError(w, "sync/mailer.html", contentMailer{
html.RenderOrError(w, "sync/mailer-3.html", contentMailer{
Config: html.NewContentConfig(),
DocumentID: doc_id.String(),
LogoURL: config.MakeURLNidus("/api/district/%s/logo", org.Slug.GetOr("unset")),
@ -80,3 +108,12 @@ func getMailerPreview(w http.ResponseWriter, r *http.Request) {
ReportURL: config.MakeURLReport("/mailer/%s", code),
})
}
func getMailerPoolRandom(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
err := platform.WriteTileRandom(ctx, w)
if err != nil {
log.Error().Err(err).Msg("failed to do random tile")
http.Error(w, "failed to do tile", http.StatusInternalServerError)
return
}
}

View file

@ -12,8 +12,13 @@ func Router() chi.Router {
// Unauthenticated endpoints
r.Get("/arcgis/oauth/begin", getArcgisOauthBegin)
r.Get("/arcgis/oauth/callback", getArcgisOauthCallback)
r.Get("/mailer/{code}", getMailer)
r.Get("/mailer/{code}/preview", getMailerPreview)
r.Get("/mailer/pool/random", getMailerPoolRandom)
r.Get("/mailer/mode-1", getMailer1)
r.Get("/mailer/mode-2/{code}", getMailer2)
r.Get("/mailer/mode-3/{code}", getMailer3)
r.Get("/mailer/mode-1/preview", getMailer1Preview)
r.Get("/mailer/mode-2/preview", getMailer2Preview)
r.Get("/mailer/mode-3/{code}/preview", getMailer3Preview)
r.Get("/district", getDistrict)
// Mock endpoints