From 632fac4558fe1feea2708ca2d263f50e3fef26ce Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 8 Jan 2026 16:05:50 +0000 Subject: [PATCH 0001/1513] Collapse public-report to a single package It was just getting annoying having it separate, and buying basically nothing. --- .../public-reports/template/nuisance.html | 541 ------------------ public-report/endpoint.go | 31 +- .../public-reports => public-report}/page.go | 6 +- .../static/vendor/css/bootstrap.min.css | 0 .../static/vendor/js/bootstrap.bundle.min.js | 0 .../static/vendor/js/bootstrap.min.js | 0 .../template/base.html | 0 .../template/component/footer.html | 0 public-report/template/nuisance.html | 501 ++++++++++++++++ .../template/pool.html | 0 .../template/quick-submit-complete.html | 0 .../template/quick.html | 0 .../register-notifications-complete.html | 0 .../template/root.html | 0 .../template/status.html | 0 15 files changed, 519 insertions(+), 560 deletions(-) delete mode 100644 htmlpage/public-reports/template/nuisance.html rename {htmlpage/public-reports => public-report}/page.go (89%) rename {htmlpage/public-reports => public-report}/static/vendor/css/bootstrap.min.css (100%) rename {htmlpage/public-reports => public-report}/static/vendor/js/bootstrap.bundle.min.js (100%) rename {htmlpage/public-reports => public-report}/static/vendor/js/bootstrap.min.js (100%) rename {htmlpage/public-reports => public-report}/template/base.html (100%) rename {htmlpage/public-reports => public-report}/template/component/footer.html (100%) create mode 100644 public-report/template/nuisance.html rename {htmlpage/public-reports => public-report}/template/pool.html (100%) rename {htmlpage/public-reports => public-report}/template/quick-submit-complete.html (100%) rename {htmlpage/public-reports => public-report}/template/quick.html (100%) rename {htmlpage/public-reports => public-report}/template/register-notifications-complete.html (100%) rename {htmlpage/public-reports => public-report}/template/root.html (100%) rename {htmlpage/public-reports => public-report}/template/status.html (100%) diff --git a/htmlpage/public-reports/template/nuisance.html b/htmlpage/public-reports/template/nuisance.html deleted file mode 100644 index 2a44adc0..00000000 --- a/htmlpage/public-reports/template/nuisance.html +++ /dev/null @@ -1,541 +0,0 @@ -{{template "base.html" .}} - -{{define "title"}}Dash{{end}} -{{define "extraheader"}} - - -{{end}} -{{define "content"}} - -
-
-
-
-

[District Name]

-
-
- -
-
-
-
- - -
-
- -
-
-

Report Mosquito Nuisance

-

Help us identify mosquito activity in your area

-
-
- - -
-
- -
-
- - -
- -
-
- -

Mosquito Activity Information

- optional -
-

The time when mosquitoes are active can help us identify the species and likely breeding sources.

- - -
-
- -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- - -
- - -
- - -
-
-
Minor
- Occasional mosquito -
-
-
Moderate
- Regular presence -
-
-
Severe
- Many mosquitoes -
-
-
- Current selection: 3/5 -
-
-
- - -
-
- - -
-
-
- - -
-
- -

Potential Mosquito Sources

- optional -
-

Have you noticed any of these common mosquito breeding sources in your area?

- -
-
-
Did you know?
-

Mosquitoes can breed in as little as a bottle cap of water! Eliminating standing water is the most effective way to reduce mosquito populations.

-
-
- -
- -
-
-
-
- -
-
Stagnant Water
-

Green pools, ponds, fountains, or birdbaths that aren't maintained

-
- - -
-
-
-
- - -
-
-
-
- -
-
Containers
-

Buckets, planters, toys, tires, or any items that collect rainwater

-
- - -
-
-
-
- - -
-
-
-
- -
-
Roof & Gutters
-

Clogged gutters, flat roofs, or AC units that collect water

-
- - -
-
-
-
-
- - - -
-
- - -
-
-
- - -
-
- -

Inspection Request

-
-

Would you like our technicians to inspect for potential mosquito sources?

- -
-
-
-
Property Inspection
-

Request a technician to inspect your property for mosquito sources. We'll contact you to schedule a convenient time.

-
- - -
-
-
- -
-
-
Neighborhood Inspection
-

Request a general inspection of your neighborhood. We'll survey the area for potential mosquito breeding sources.

-
- - -
-
-
-
- - - -
- - -
-
- -

Location & Contact Information

-
- -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
We'll use this to send you a confirmation and follow-up information.
-
-
-
- - -
-
- -

Additional Information

- optional -
- -
-
- - -
-
-
- - -
-
-
-

Thank you for reporting this mosquito issue.

-

After submission, you'll receive a confirmation with a report ID and further information.

-
-
- -
-
-
-
- - - -
-
- - - -{{end}} diff --git a/public-report/endpoint.go b/public-report/endpoint.go index 374cdd72..bb872b00 100644 --- a/public-report/endpoint.go +++ b/public-report/endpoint.go @@ -12,7 +12,6 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/h3utils" "github.com/Gleipnir-Technology/nidus-sync/htmlpage" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage/public-reports" "github.com/Gleipnir-Technology/nidus-sync/userfile" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" @@ -35,45 +34,45 @@ func Router() chi.Router { r.Get("/register-notifications-complete", getRegisterNotificationsComplete) r.Get("/status", getStatus) localFS := http.Dir("./static") - htmlpage.FileServer(r, "/static", localFS, publicreports.EmbeddedStaticFS, "static") + htmlpage.FileServer(r, "/static", localFS, EmbeddedStaticFS, "static") return r } func getRoot(w http.ResponseWriter, r *http.Request) { htmlpage.RenderOrError( w, - publicreports.Root, - publicreports.ContextRoot{}, + Root, + ContextRoot{}, ) } func getNuisance(w http.ResponseWriter, r *http.Request) { htmlpage.RenderOrError( w, - publicreports.Nuisance, - publicreports.ContextNuisance{}, + Nuisance, + ContextNuisance{}, ) } func getPool(w http.ResponseWriter, r *http.Request) { htmlpage.RenderOrError( w, - publicreports.Pool, - publicreports.ContextPool{}, + Pool, + ContextPool{}, ) } func getQuick(w http.ResponseWriter, r *http.Request) { htmlpage.RenderOrError( w, - publicreports.Quick, - publicreports.ContextQuick{}, + Quick, + ContextQuick{}, ) } func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) { report := r.URL.Query().Get("report") htmlpage.RenderOrError( w, - publicreports.QuickSubmitComplete, - publicreports.ContextQuickSubmitComplete{ + QuickSubmitComplete, + ContextQuickSubmitComplete{ ReportID: report, }, ) @@ -82,8 +81,8 @@ func getRegisterNotificationsComplete(w http.ResponseWriter, r *http.Request) { report := r.URL.Query().Get("report") htmlpage.RenderOrError( w, - publicreports.RegisterNotificationsComplete, - publicreports.ContextRegisterNotificationsComplete{ + RegisterNotificationsComplete, + ContextRegisterNotificationsComplete{ ReportID: report, }, ) @@ -91,8 +90,8 @@ func getRegisterNotificationsComplete(w http.ResponseWriter, r *http.Request) { func getStatus(w http.ResponseWriter, r *http.Request) { htmlpage.RenderOrError( w, - publicreports.Status, - publicreports.ContextStatus{}, + Status, + ContextStatus{}, ) } func postQuick(w http.ResponseWriter, r *http.Request) { diff --git a/htmlpage/public-reports/page.go b/public-report/page.go similarity index 89% rename from htmlpage/public-reports/page.go rename to public-report/page.go index 9f4d2188..e44c7489 100644 --- a/htmlpage/public-reports/page.go +++ b/public-report/page.go @@ -1,4 +1,4 @@ -package publicreports +package publicreport import ( "embed" @@ -38,7 +38,7 @@ var ( var components = [...]string{"footer"} func buildTemplate(files ...string) *htmlpage.BuiltTemplate { - subdir := "htmlpage/public-reports" + subdir := "public-report" full_files := make([]string, 0) for _, f := range files { full_files = append(full_files, fmt.Sprintf("%s/template/%s.html", subdir, f)) @@ -46,5 +46,5 @@ func buildTemplate(files ...string) *htmlpage.BuiltTemplate { for _, c := range components { full_files = append(full_files, fmt.Sprintf("%s/template/component/%s.html", subdir, c)) } - return htmlpage.NewBuiltTemplate(embeddedFiles, "htmlpage/public-reports/", full_files...) + return htmlpage.NewBuiltTemplate(embeddedFiles, "public-report/", full_files...) } diff --git a/htmlpage/public-reports/static/vendor/css/bootstrap.min.css b/public-report/static/vendor/css/bootstrap.min.css similarity index 100% rename from htmlpage/public-reports/static/vendor/css/bootstrap.min.css rename to public-report/static/vendor/css/bootstrap.min.css diff --git a/htmlpage/public-reports/static/vendor/js/bootstrap.bundle.min.js b/public-report/static/vendor/js/bootstrap.bundle.min.js similarity index 100% rename from htmlpage/public-reports/static/vendor/js/bootstrap.bundle.min.js rename to public-report/static/vendor/js/bootstrap.bundle.min.js diff --git a/htmlpage/public-reports/static/vendor/js/bootstrap.min.js b/public-report/static/vendor/js/bootstrap.min.js similarity index 100% rename from htmlpage/public-reports/static/vendor/js/bootstrap.min.js rename to public-report/static/vendor/js/bootstrap.min.js diff --git a/htmlpage/public-reports/template/base.html b/public-report/template/base.html similarity index 100% rename from htmlpage/public-reports/template/base.html rename to public-report/template/base.html diff --git a/htmlpage/public-reports/template/component/footer.html b/public-report/template/component/footer.html similarity index 100% rename from htmlpage/public-reports/template/component/footer.html rename to public-report/template/component/footer.html diff --git a/public-report/template/nuisance.html b/public-report/template/nuisance.html new file mode 100644 index 00000000..9b8fafdc --- /dev/null +++ b/public-report/template/nuisance.html @@ -0,0 +1,501 @@ +{{template "base.html" .}} + +{{define "title"}}Dash{{end}} +{{define "extraheader"}} + + +{{end}} +{{define "content"}} +
+ +
+
+

Report Mosquito Nuisance

+

Help us identify mosquito activity in your area

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

Mosquito Activity Information

+ optional +
+

The time when mosquitoes are active can help us identify the species and likely breeding sources.

+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+ + +
+ + +
+
+
Minor
+ Occasional mosquito +
+
+
Moderate
+ Regular presence +
+
+
Severe
+ Many mosquitoes +
+
+
+ Current selection: 3/5 +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +

Potential Mosquito Sources

+ optional +
+

Have you noticed any of these common mosquito breeding sources in your area?

+ +
+
+
Did you know?
+

Mosquitoes can breed in as little as a bottle cap of water! Eliminating standing water is the most effective way to reduce mosquito populations.

+
+
+ +
+ +
+
+
+
+ +
+
Stagnant Water
+

Green pools, ponds, fountains, or birdbaths that aren't maintained

+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
Containers
+

Buckets, planters, toys, tires, or any items that collect rainwater

+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
Roof & Gutters
+

Clogged gutters, flat roofs, or AC units that collect water

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

Inspection Request

+
+

Would you like our technicians to inspect for potential mosquito sources?

+ +
+
+
+
Property Inspection
+

Request a technician to inspect your property for mosquito sources. We'll contact you to schedule a convenient time.

+
+ + +
+
+
+ +
+
+
Neighborhood Inspection
+

Request a general inspection of your neighborhood. We'll survey the area for potential mosquito breeding sources.

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

Location & Contact Information

+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
We'll use this to send you a confirmation and follow-up information.
+
+
+
+ + +
+
+ +

Additional Information

+ optional +
+ +
+
+ + +
+
+
+ + +
+
+
+

Thank you for reporting this mosquito issue.

+

After submission, you'll receive a confirmation with a report ID and further information.

+
+
+ +
+
+
+
+
+{{end}} diff --git a/htmlpage/public-reports/template/pool.html b/public-report/template/pool.html similarity index 100% rename from htmlpage/public-reports/template/pool.html rename to public-report/template/pool.html diff --git a/htmlpage/public-reports/template/quick-submit-complete.html b/public-report/template/quick-submit-complete.html similarity index 100% rename from htmlpage/public-reports/template/quick-submit-complete.html rename to public-report/template/quick-submit-complete.html diff --git a/htmlpage/public-reports/template/quick.html b/public-report/template/quick.html similarity index 100% rename from htmlpage/public-reports/template/quick.html rename to public-report/template/quick.html diff --git a/htmlpage/public-reports/template/register-notifications-complete.html b/public-report/template/register-notifications-complete.html similarity index 100% rename from htmlpage/public-reports/template/register-notifications-complete.html rename to public-report/template/register-notifications-complete.html diff --git a/htmlpage/public-reports/template/root.html b/public-report/template/root.html similarity index 100% rename from htmlpage/public-reports/template/root.html rename to public-report/template/root.html diff --git a/htmlpage/public-reports/template/status.html b/public-report/template/status.html similarity index 100% rename from htmlpage/public-reports/template/status.html rename to public-report/template/status.html From a162781f89d24b42897a8bca981be26b87f75b43 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 8 Jan 2026 16:08:41 +0000 Subject: [PATCH 0002/1513] Metadata fixes for various pages --- public-report/template/base.html | 2 +- public-report/template/nuisance.html | 2 +- public-report/template/quick-submit-complete.html | 2 +- public-report/template/quick.html | 2 +- public-report/template/register-notifications-complete.html | 2 +- public-report/template/root.html | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public-report/template/base.html b/public-report/template/base.html index 61abeec2..2fd9c58a 100644 --- a/public-report/template/base.html +++ b/public-report/template/base.html @@ -3,7 +3,7 @@ - {{template "title" .}} - Nidus Sync + {{template "title" .}} - Report Mosquitoes Online diff --git a/public-report/template/nuisance.html b/public-report/template/nuisance.html index 9b8fafdc..5ab5fe2f 100644 --- a/public-report/template/nuisance.html +++ b/public-report/template/nuisance.html @@ -1,6 +1,6 @@ {{template "base.html" .}} -{{define "title"}}Dash{{end}} +{{define "title"}}Nuisance{{end}} {{define "extraheader"}} +{{end}} +{{define "content"}} +
+
+
+ +
+
+

+ + + + Nuisance Report Complete +

+
+
+
+
+ + + +
+

Thank You!

+

Your report has been successfully submitted.

+
+ Report ID: + {{.ReportID}} +
+
+ +
+ + +
+
+ + + + + What to Expect +
+
    +
  • + + + + + A confirmation message has been sent to your contact information. +
  • +
  • + + + + + + You will receive updates when: +
      +
    • Your report is assigned to a specialist
    • +
    • A site visit is scheduled
    • +
    • Treatment or remediation is completed
    • +
    • The case is resolved
    • +
    +
  • +
  • + + + + You can check your report status anytime using your Report ID. +
  • +
+
+ + + +
+
+ + +
+
+
+ + + + + Need Help? +
+

If you need to update your contact information or have questions about your report, please contact our Mosquito Control Unit at (123) 456-7890 or mosquito@example.gov and reference your Report ID.

+
+
+
+
+
+{{end}} diff --git a/public-report/template/nuisance.html b/public-report/template/nuisance.html index 5ab5fe2f..84f34b6f 100644 --- a/public-report/template/nuisance.html +++ b/public-report/template/nuisance.html @@ -163,7 +163,7 @@ document.addEventListener('DOMContentLoaded', function() { -
+
@@ -179,7 +179,7 @@ document.addEventListener('DOMContentLoaded', function() {
- +
- +
- +
- +
From 9e586ae6efc4f67f522206beb478ab177b871f15 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 20:45:51 +0000 Subject: [PATCH 0120/1513] Update wording for district landing page At Ben's request --- public-report/template/mock/district-root.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public-report/template/mock/district-root.html b/public-report/template/mock/district-root.html index 68a0b33e..42ca10a0 100644 --- a/public-report/template/mock/district-root.html +++ b/public-report/template/mock/district-root.html @@ -35,10 +35,10 @@
-

Report Mosquitoes for {{.District.Name}}

+

Report a Mosquito Problem

-

This is the reporting page for mosquito problems in your area.

-

Reports submitted here are reviewed by {{.District.Name}}, which helps identify and address mosquito problems in your community.

+

Submit a report to help reduce mosquito activity in your neighborhood.

+

Report Mosquitoes Online works with local mosquito control agencies to receive public reports. For this area, mosquito control services are provided by Delta Mosquito and Vector Control District.

From 9aee938e30b6ab7ec8befb45488f99809c3ac606 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:00:30 +0000 Subject: [PATCH 0121/1513] Add status searching page --- public-report/mock.go | 8 +- .../template/mock/district-root.html | 2 +- public-report/template/mock/status.html | 204 ++++++++++++++++++ 3 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 public-report/template/mock/status.html diff --git a/public-report/mock.go b/public-report/mock.go index 83d2805f..d7b10ce0 100644 --- a/public-report/mock.go +++ b/public-report/mock.go @@ -13,6 +13,7 @@ var ( mockNuisanceT = buildTemplate("mock/nuisance", "base") mockNuisanceSubmitCompleteT = buildTemplate("mock/nuisance-submit-complete", "base") mockRootT = buildTemplate("mock/root", "base") + mockStatusT = buildTemplate("mock/status", "base") ) type ContentDistrict struct { @@ -22,6 +23,8 @@ type ContentDistrict struct { type ContentURL struct { Nuisance string NuisanceSubmitComplete string + Status string + Tegola string } type ContentMock struct { District ContentDistrict @@ -32,15 +35,18 @@ type ContentMock struct { func addMockRoutes(r chi.Router) { r.Get("/", renderMock(mockRootT)) + r.Get("/district/{slug}", renderMock(mockDistrictRootT)) r.Get("/nuisance", renderMock(mockNuisanceT)) r.Get("/nuisance-submit-complete", renderMock(mockNuisanceSubmitCompleteT)) - r.Get("/district/{slug}", renderMock(mockDistrictRootT)) + r.Get("/status", renderMock(mockStatusT)) } func makeContentURL() ContentURL { return ContentURL{ Nuisance: makeURLMock("nuisance"), NuisanceSubmitComplete: makeURLMock("nuisance-submit-complete"), + Status: makeURLMock("status"), + Tegola: config.MakeURLTegola("/"), } } diff --git a/public-report/template/mock/district-root.html b/public-report/template/mock/district-root.html index 42ca10a0..8cb54251 100644 --- a/public-report/template/mock/district-root.html +++ b/public-report/template/mock/district-root.html @@ -81,7 +81,7 @@

Follow-up or Check Status

Check on a previous request or view current mosquito activity in your area.

- Get Status + Get Status
diff --git a/public-report/template/mock/status.html b/public-report/template/mock/status.html new file mode 100644 index 00000000..2b8dd8f2 --- /dev/null +++ b/public-report/template/mock/status.html @@ -0,0 +1,204 @@ +{{template "base.html" .}} + +{{define "title"}}Status{{end}} +{{define "extraheader"}} + + + + + + + +{{end}} +{{define "content"}} +
+ + + + +
+
+
Reports Map
+
+
+ +
+
+ + +
+
+
Reports Near You
+ 15 Reports Found +
+
+
+ +
+
+ +
+
+{{end}} From 65c3e8ee5198d3f339b251cb41e6c63a0268379e Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:29:24 +0000 Subject: [PATCH 0122/1513] Add branded header for nuisance report --- public-report/mock.go | 19 +++++++++++-------- public-report/page.go | 2 +- public-report/template/component/header.html | 13 +++++++++++++ public-report/template/mock/nuisance.html | 3 +++ 4 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 public-report/template/component/header.html diff --git a/public-report/mock.go b/public-report/mock.go index d7b10ce0..2d7b3469 100644 --- a/public-report/mock.go +++ b/public-report/mock.go @@ -36,22 +36,25 @@ type ContentMock struct { func addMockRoutes(r chi.Router) { r.Get("/", renderMock(mockRootT)) r.Get("/district/{slug}", renderMock(mockDistrictRootT)) + r.Get("/district/{slug}/nuisance", renderMock(mockNuisanceT)) + r.Get("/district/{slug}/nuisance-submit-complete", renderMock(mockNuisanceSubmitCompleteT)) + r.Get("/district/{slug}/status", renderMock(mockStatusT)) r.Get("/nuisance", renderMock(mockNuisanceT)) r.Get("/nuisance-submit-complete", renderMock(mockNuisanceSubmitCompleteT)) r.Get("/status", renderMock(mockStatusT)) } -func makeContentURL() ContentURL { +func makeContentURL(slug string) ContentURL { return ContentURL{ - Nuisance: makeURLMock("nuisance"), - NuisanceSubmitComplete: makeURLMock("nuisance-submit-complete"), - Status: makeURLMock("status"), + Nuisance: makeURLMock(slug, "nuisance"), + NuisanceSubmitComplete: makeURLMock(slug, "nuisance-submit-complete"), + Status: makeURLMock(slug, "status"), Tegola: config.MakeURLTegola("/"), } } -func makeURLMock(p string) string { - return config.MakeURLReport("/mock/%s", p) +func makeURLMock(slug, p string) string { + return config.MakeURLReport("/mock/district/%s/%s", slug, p) } func renderMock(t *htmlpage.BuiltTemplate) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { @@ -64,12 +67,12 @@ func renderMock(t *htmlpage.BuiltTemplate) func(http.ResponseWriter, *http.Reque t, ContentMock{ District: ContentDistrict{ - Name: "Delta MCD", + Name: "Delta MVCD", URLLogo: config.MakeURLNidus("/api/district/%s/logo", slug), }, MapboxToken: config.MapboxToken, ReportID: "abcd-1234-5678", - URL: makeContentURL(), + URL: makeContentURL(slug), }, ) } diff --git a/public-report/page.go b/public-report/page.go index 0b166703..2785e7e6 100644 --- a/public-report/page.go +++ b/public-report/page.go @@ -10,7 +10,7 @@ import ( //go:embed template/* var embeddedFiles embed.FS -var components = [...]string{"footer", "photo-upload", "photo-upload-header"} +var components = [...]string{"footer", "header", "photo-upload", "photo-upload-header"} var svgs = [...]string{"check-report", "mosquito", "pond"} func buildTemplate(files ...string) *htmlpage.BuiltTemplate { diff --git a/public-report/template/component/header.html b/public-report/template/component/header.html new file mode 100644 index 00000000..453a2aa3 --- /dev/null +++ b/public-report/template/component/header.html @@ -0,0 +1,13 @@ +{{define "header"}} + + +{{end}} diff --git a/public-report/template/mock/nuisance.html b/public-report/template/mock/nuisance.html index ed22a714..0883a3f7 100644 --- a/public-report/template/mock/nuisance.html +++ b/public-report/template/mock/nuisance.html @@ -236,6 +236,9 @@ document.addEventListener('DOMContentLoaded', function() { {{end}} {{define "content"}} +{{if .District}} + {{template "header" .}} +{{end}}
From c6fd6295a078e659172017afc2c10d132ac99039 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:30:25 +0000 Subject: [PATCH 0123/1513] Add district header to seach page satisfies a requirement. --- public-report/template/mock/status.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public-report/template/mock/status.html b/public-report/template/mock/status.html index 2b8dd8f2..eeab00c5 100644 --- a/public-report/template/mock/status.html +++ b/public-report/template/mock/status.html @@ -125,6 +125,9 @@ document.addEventListener('DOMContentLoaded', onLoad); {{end}} {{define "content"}} +{{if .District}} + {{template "header" .}} +{{end}}

Report Standing Water

Report any water that has been sitting for several days, where mosquitoes can live.

- Report Source + Report Water
diff --git a/public-report/template/mock/water.html b/public-report/template/mock/water.html new file mode 100644 index 00000000..52926830 --- /dev/null +++ b/public-report/template/mock/water.html @@ -0,0 +1,569 @@ +{{template "base.html" .}} + +{{define "title"}}Green Pool{{end}} +{{define "extraheader"}} + + + + + + +{{template "photo-upload-header"}} + + +{{end}} +{{define "content"}} +{{if .District}} + {{template "header" .}} +{{end}} + +
+
+ +
+
+

Report Standing Water

+

Help us locate and treat potential mosquito production sources in your area

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

Photos

+
+

Photos help us identify the severity of the issue and may contain location data that can help us find the source.

+
+ {{template "photo-upload"}} +
+
+ + +
+
+ +

Location

+
+

Please provide the location of the potential mosquito production source. We may be able to extract this information from your photos if they contain location data.

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

You can also click on the map to mark the location precisely

+ + +
+ + +
+
+ +

Source Details

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

Access Information

+
+

Please provide any details about how to access the mosquito source. This helps our technicians when they visit the site.

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

Contact Information

+
+ + +
Property Owner Information (if known)
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Your Contact Information (for updates)
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ + +
+
+ +

Additional Information

+
+

Please provide any other information that might help us address this mosquito source.

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

Thank you for helping us keep our community safe from mosquito-borne illnesses.

+

After submission, you will receive a confirmation with a report ID for tracking purposes.

+
+
+ +
+
+
+
+ + + +
+
+ + + +{{end}} From d552a18c0b63e0f8afdfcb80b8f353a5cae6adad Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:43:51 +0000 Subject: [PATCH 0125/1513] Remove location details by request --- public-report/template/mock/water.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/public-report/template/mock/water.html b/public-report/template/mock/water.html index 52926830..7542f5f5 100644 --- a/public-report/template/mock/water.html +++ b/public-report/template/mock/water.html @@ -215,7 +215,6 @@ document.addEventListener('DOMContentLoaded', function() { console.log("location error", error); }) }) - mapLocator.addEventListener("markerdragend", let mapZoom = document.getElementById('map-zoom'); mapLocator.addEventListener("zoomend", function(e) { mapZoom.value = e.target.getZoom(); @@ -316,9 +315,6 @@ function displaySelectedCoordinates(lngLat) {
-
- -

You can also click on the map to mark the location precisely

From d59c6197292f1f00e084958ca2023d123dbb89bd Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:47:24 +0000 Subject: [PATCH 0126/1513] Move additional details to below photo as requested --- public-report/template/mock/water.html | 39 +++++++++++++------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/public-report/template/mock/water.html b/public-report/template/mock/water.html index 7542f5f5..c86023bd 100644 --- a/public-report/template/mock/water.html +++ b/public-report/template/mock/water.html @@ -275,19 +275,36 @@ function displaySelectedCoordinates(lngLat) { -
+

Photos

-

Photos help us identify the severity of the issue and may contain location data that can help us find the source.

+

Photos help us identify the severity of the issue and may contain location data that can help us find the production source.

{{template "photo-upload"}}
+ +
+
+ +

Additional Information

+
+

Please provide any other information that might help us address this mosquito production source.

+ +
+
+ + +
+
+
+ +
@@ -331,7 +348,7 @@ function displaySelectedCoordinates(lngLat) {
- + -
-
-
-
From 135e2ef77d634a539180c61408d962247d048708 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:50:28 +0000 Subject: [PATCH 0127/1513] Put additional fields behind collapse button --- public-report/template/mock/water.html | 256 +++++++++++++------------ 1 file changed, 135 insertions(+), 121 deletions(-) diff --git a/public-report/template/mock/water.html b/public-report/template/mock/water.html index c86023bd..221f2b76 100644 --- a/public-report/template/mock/water.html +++ b/public-report/template/mock/water.html @@ -162,6 +162,14 @@ function setLocationInputs(location) { street.value = context.country.name; } +function toggleCollapse(something) { + el = document.getElementById(something) + if (el.classList.contains('collapse')) { + el.classList.remove('collapse'); + } else { + el.classList.add('collapse'); + } +} document.addEventListener('DOMContentLoaded', function() { // Elements const photoInput = document.getElementById('photos'); @@ -339,145 +347,151 @@ function displaySelectedCoordinates(lngLat) {
- -
-
- -

Source Details

-
- -
-
- - + +
+ + +
+
+ +

Source Details

-
- -
- - +
+
+ +
-
- - -
-
- - + +
+ +
+ + +
+
+ + +
+
+ + +
-
- -
-
- -

Access Information

-
-

Please provide any details about how to access the mosquito source. This helps our technicians when they visit the site.

- -
-
- - + +
+
+ +

Access Information

-
- -
-
- -
-
-
- - +

Please provide any details about how to access the mosquito source. This helps our technicians when they visit the site.

+ +
+
+ + +
+
+ +
+
+ +
+
+
+ + +
+
+ + +
-
- - +
+
+ + +
+
+ + +
-
-
-
- - -
-
- - -
-
-
-
- - +
+
+ + +
-
- -
-
- -

Contact Information

-
- - -
Property Owner Information (if known)
-
-
- - + +
+
+ +

Contact Information

-
- - + + +
Property Owner Information (if known)
+
+
+ + +
+
+ + +
+
+ + +
-
- - -
-
- - -
Your Contact Information (for updates)
-
-
- - -
-
- - -
-
- - -
-
-
- - + + +
Your Contact Information (for updates)
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
From 0407f270ade6d67b634bea4c79c125460494cdb4 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:52:54 +0000 Subject: [PATCH 0128/1513] Fix page title for standing water --- public-report/template/mock/water.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public-report/template/mock/water.html b/public-report/template/mock/water.html index 221f2b76..53cdc550 100644 --- a/public-report/template/mock/water.html +++ b/public-report/template/mock/water.html @@ -1,6 +1,6 @@ {{template "base.html" .}} -{{define "title"}}Green Pool{{end}} +{{define "title"}}Standing Water{{end}} {{define "extraheader"}} From 16477a9f5aee938c362c54f97f05d7e3b9c1b92b Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:53:06 +0000 Subject: [PATCH 0129/1513] Remove back button --- public-report/template/mock/water.html | 9 --------- 1 file changed, 9 deletions(-) diff --git a/public-report/template/mock/water.html b/public-report/template/mock/water.html index 53cdc550..aed3ad70 100644 --- a/public-report/template/mock/water.html +++ b/public-report/template/mock/water.html @@ -512,15 +512,6 @@ function displaySelectedCoordinates(lngLat) {
- - -
From dd57cacd3b0c5a035537a992540be6841d72d391 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:55:10 +0000 Subject: [PATCH 0130/1513] Link up water report with confirmation page --- public-report/template/mock/water.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public-report/template/mock/water.html b/public-report/template/mock/water.html index aed3ad70..dd5d244d 100644 --- a/public-report/template/mock/water.html +++ b/public-report/template/mock/water.html @@ -505,9 +505,9 @@ function displaySelectedCoordinates(lngLat) {

After submission, you will receive a confirmation with a report ID for tracking purposes.

From 1f52dda56ded4fa83120f95eea7ce9a034c10947 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 21:58:22 +0000 Subject: [PATCH 0131/1513] Fix bug icon. --- public-report/template/mock/nuisance.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public-report/template/mock/nuisance.html b/public-report/template/mock/nuisance.html index 0883a3f7..426ef1ff 100644 --- a/public-report/template/mock/nuisance.html +++ b/public-report/template/mock/nuisance.html @@ -284,7 +284,7 @@ document.addEventListener('DOMContentLoaded', function() {
- + {{ template "svg/mosquito" }}

Mosquito Activity Information

optional
From 5e9c0d9f11201cd5625d236eb80a6f8ba8edd7fe Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sat, 24 Jan 2026 22:00:08 +0000 Subject: [PATCH 0132/1513] Remove the address display By request --- public-report/template/mock/nuisance.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/public-report/template/mock/nuisance.html b/public-report/template/mock/nuisance.html index 426ef1ff..0cb6e57b 100644 --- a/public-report/template/mock/nuisance.html +++ b/public-report/template/mock/nuisance.html @@ -273,9 +273,6 @@ document.addEventListener('DOMContentLoaded', function() {
-
- -

You can also click on the map to mark the location precisely

From c0b6398de283b15d069831b443fc2a309198a4b9 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sun, 25 Jan 2026 18:47:22 +0000 Subject: [PATCH 0133/1513] Overhaul text messaging system to be like emails It's a better system for organization and makes it so we can have better logs about what gets sent. --- background/background.go | 3 +- background/text.go | 54 ++----- comms/text/db.go | 93 ++++++++++++ comms/text/job.go | 39 +++++ comms/text/report-subscription.go | 56 +++++++ comms/{ => text}/text.go | 19 ++- db/dberrors/comms.text_log.bob.go | 2 +- db/dberrors/organization.bob.go | 9 ++ db/dbinfo/comms.text_log.bob.go | 52 ++++--- db/dbinfo/organization.bob.go | 28 +++- db/enums/enums.bob.go | 57 ++++--- db/factory/bobfactory_main.bob.go | 4 +- db/factory/bobfactory_random.bob.go | 4 +- db/factory/comms.text_log.bob.go | 172 ++++++++++++++++------ db/migrations/00041_text_log_overhaul.sql | 34 +++++ db/models/comms.text_log.bob.go | 162 ++++++++++++-------- public-report/quick.go | 4 +- public-report/template/mock/nuisance.html | 2 +- public-report/template/mock/water.html | 2 +- 19 files changed, 577 insertions(+), 219 deletions(-) create mode 100644 comms/text/db.go create mode 100644 comms/text/job.go create mode 100644 comms/text/report-subscription.go rename comms/{ => text}/text.go (74%) create mode 100644 db/migrations/00041_text_log_overhaul.sql diff --git a/background/background.go b/background/background.go index 6afdabb4..a5c44584 100644 --- a/background/background.go +++ b/background/background.go @@ -5,6 +5,7 @@ import ( "sync" "github.com/Gleipnir-Technology/nidus-sync/comms/email" + "github.com/Gleipnir-Technology/nidus-sync/comms/text" ) var waitGroup sync.WaitGroup @@ -14,7 +15,7 @@ func Start(ctx context.Context) { channelJobAudio = make(chan jobAudio, 100) // Buffered channel to prevent blocking channelJobEmail = make(chan email.Job, 100) // Buffered channel to prevent blocking - channelJobText = make(chan jobText, 100) // Buffered channel to prevent blocking + channelJobText = make(chan text.Job, 100) // Buffered channel to prevent blocking waitGroup.Add(1) go func() { diff --git a/background/text.go b/background/text.go index 853f7041..80de3b2b 100644 --- a/background/text.go +++ b/background/text.go @@ -2,34 +2,23 @@ package background import ( "context" - "errors" - "fmt" - "github.com/Gleipnir-Technology/nidus-sync/comms" + "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/rs/zerolog/log" ) -var channelJobText chan jobText +var channelJobText chan text.Job -func ReportSubscriptionConfirmationText(destination comms.E164, report_id string) { - enqueueJobText(jobText{ - Destination: destination, - ReportID: report_id, - Source: config.RMOPhoneNumber, - Type: enums.CommsMessagetypetextReportSubscriptionConfirmation, - }) +func ReportSubscriptionConfirmationText(destination text.E164, report_id string) { + enqueueJobText(text.NewJobReportSubscriptionConfirmation( + destination, + report_id, + config.RMOPhoneNumber, + )) } -type jobText struct { - Destination comms.E164 - ReportID string - Source comms.E164 - Type enums.CommsMessagetypetext -} - -func enqueueJobText(job jobText) { +func enqueueJobText(job text.Job) { select { case channelJobText <- job: log.Info().Msg("Enqueued text job") @@ -38,7 +27,7 @@ func enqueueJobText(job jobText) { } } -func startWorkerText(ctx context.Context, channel chan jobText) { +func startWorkerText(ctx context.Context, channel chan text.Job) { go func() { for { select { @@ -46,29 +35,8 @@ func startWorkerText(ctx context.Context, channel chan jobText) { log.Info().Msg("Email worker shutting down.") return case job := <-channel: - err := jobProcessText(job) - if err != nil { - log.Error().Err(err).Str("type", string(job.Type)).Msg("Error processing text message job") - } + text.Handle(ctx, job) } } }() } - -func jobProcessText(job jobText) error { - var message string - switch job.Type { - case enums.CommsMessagetypetextInitialContact: - message = "This is Report Mosquitoes Online. We just got your number. Text \"YES\" to get texts, or \"STOP\" to stap." - case enums.CommsMessagetypetextReportSubscriptionConfirmation: - message = "Thanks for submitting a mosquito report. Text for any questions. We'll send you updates as we get them." - default: - return errors.New("No idea what message to send") - } - err := comms.SendText(job.Source, job.Destination, message) - if err != nil { - log.Error().Err(err).Msg("Failed to send text message") - return fmt.Errorf("Failed to send message '%s' to '%s'", job.Type, job.Destination) - } - return nil -} diff --git a/comms/text/db.go b/comms/text/db.go new file mode 100644 index 00000000..5b609df9 --- /dev/null +++ b/comms/text/db.go @@ -0,0 +1,93 @@ +package text + +import ( + "context" + "crypto/sha256" + "database/sql" + "encoding/hex" + "fmt" + "sort" + "strings" + "time" + + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/aarondl/opt/omit" + "github.com/rs/zerolog/log" + "github.com/stephenafamo/bob/types/pgtypes" +) + +func convertToPGData(data map[string]string) pgtypes.HStore { + result := pgtypes.HStore{} + for k, v := range data { + result[k] = sql.Null[string]{V: v, Valid: true} + } + return result +} + +func ensureInDB(ctx context.Context, destination string) (err error) { + _, err = models.FindCommsPhone(ctx, db.PGInstance.BobDB, destination) + if err != nil { + // doesn't exist + if err.Error() == "sql: no rows in result set" { + _, err = models.CommsPhones.Insert(&models.CommsPhoneSetter{ + E164: omit.From(destination), + IsSubscribed: omit.From(false), + }).One(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to insert new phone contact: %w", err) + } + log.Info().Str("phone", destination).Msg("Added text to the comms database") + return nil + } + return fmt.Errorf("Unexpected error searching for phone contact: %w", err) + } + return nil +} + +func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin) (err error) { + _, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ + //ID: + Content: omit.From(content), + Created: omit.From(time.Now()), + Destination: omit.From(destination), + Origin: omit.From(origin), + Source: omit.From(source), + }).One(ctx, db.PGInstance.BobDB) + + return err +} +func generatePublicId(t enums.CommsMessagetypeemail, m map[string]string) string { + if m == nil || len(m) == 0 { + // Return hash of empty string for empty maps + emptyHash := sha256.Sum256([]byte("")) + return hex.EncodeToString(emptyHash[:]) + } + + // Get and sort keys for deterministic ordering + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + + // Build a string with all key-value pairs + var sb strings.Builder + // Add type first + sb.WriteString(fmt.Sprintf("type:%s,", t)) + for _, k := range keys { + sb.WriteString(k) + sb.WriteString(":") // Separator between key and value + sb.WriteString(m[k]) + sb.WriteString(",") // Separator between pairs + } + + // Compute SHA-256 hash + hasher := sha256.New() + hasher.Write([]byte(sb.String())) + hashBytes := hasher.Sum(nil) + + // Convert to hex string and return + return hex.EncodeToString(hashBytes) +} diff --git a/comms/text/job.go b/comms/text/job.go new file mode 100644 index 00000000..4d2bfdf3 --- /dev/null +++ b/comms/text/job.go @@ -0,0 +1,39 @@ +package text + +import ( + "context" + + "github.com/rs/zerolog/log" +) + +type MessageType int + +const ( + ReportSubscription MessageType = iota +) + +type Job interface { + content() string + destination() string + messageType() MessageType + messageTypeName() string + source() string +} + +func Handle(ctx context.Context, job Job) { + var err error + switch job.messageType() { + case ReportSubscription: + err = sendReportSubscription(ctx, job) + } + if err != nil { + log.Error().Err(err).Str("dest", job.destination()).Str("type", string(job.messageTypeName())).Msg("Error processing email") + return + } + /* + case enums.CommsMessagetypeemailReportStatusScheduled: + case enums.CommsMessagetypeemailReportStatusComplete: + + } + */ +} diff --git a/comms/text/report-subscription.go b/comms/text/report-subscription.go new file mode 100644 index 00000000..1dd1f193 --- /dev/null +++ b/comms/text/report-subscription.go @@ -0,0 +1,56 @@ +package text + +import ( + "context" + "fmt" + + "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/nyaruka/phonenumbers" + //"github.com/rs/zerolog/log" +) + +func NewJobReportSubscriptionConfirmation( + destination E164, + report_id string, + source E164) jobReportSubscription { + return jobReportSubscription{ + dst: destination, + reportID: report_id, + src: source, + } +} + +type jobReportSubscription struct { + dst E164 + reportID string + src E164 +} + +func (j jobReportSubscription) content() string { + return fmt.Sprintf("Thanks for submitting mosquito report %s. Text for any questions. We'll send you updates as we get them.", j.reportID) +} +func (j jobReportSubscription) destination() string { + return phonenumbers.Format(&j.dst, phonenumbers.E164) +} +func (j jobReportSubscription) messageType() MessageType { + return ReportSubscription +} +func (j jobReportSubscription) messageTypeName() string { + return "report-subscription" +} +func (j jobReportSubscription) source() string { + return phonenumbers.Format(&j.src, phonenumbers.E164) +} + +func sendReportSubscription(ctx context.Context, job Job) error { + j, ok := job.(jobReportSubscription) + if !ok { + return fmt.Errorf("job is not for report subscription confirmation") + } + + err := sendText(ctx, j.src, j.dst, j.content(), enums.CommsTextoriginWebsiteAction) + if err != nil { + return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + } + return nil +} diff --git a/comms/text.go b/comms/text/text.go similarity index 74% rename from comms/text.go rename to comms/text/text.go index 5dfce308..e0846d2b 100644 --- a/comms/text.go +++ b/comms/text/text.go @@ -1,10 +1,12 @@ -package comms +package text import ( + "context" "encoding/json" "fmt" "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/nyaruka/phonenumbers" "github.com/rs/zerolog/log" "github.com/twilio/twilio-go" @@ -17,16 +19,23 @@ func ParsePhoneNumber(input string) (*E164, error) { return phonenumbers.Parse(input, "US") } -func SendText(source E164, destination E164, message string) error { - log.Info().Msg("Sending text message...") - // Make sure TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN exists in your environment +func sendText(ctx context.Context, source E164, destination E164, message string, origin enums.CommsTextorigin) error { + src := phonenumbers.Format(&source, phonenumbers.E164) + dest := phonenumbers.Format(&destination, phonenumbers.E164) + err := ensureInDB(ctx, dest) + if err != nil { + return fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err) + } + err = insertTextLog(ctx, message, dest, src, origin) + if err != nil { + return fmt.Errorf("Failed to insert text message in the DB: %w", err) + } client := twilio.NewRestClient() params := &twilioApi.CreateMessageParams{} params.SetMessagingServiceSid(config.TwilioMessagingServiceSID) params.SetBody(message) - dest := phonenumbers.Format(&destination, phonenumbers.E164) params.SetTo(dest) resp, err := client.Api.CreateMessage(params) diff --git a/db/dberrors/comms.text_log.bob.go b/db/dberrors/comms.text_log.bob.go index 4611eb7a..8bd6a9dc 100644 --- a/db/dberrors/comms.text_log.bob.go +++ b/db/dberrors/comms.text_log.bob.go @@ -7,7 +7,7 @@ var CommsTextLogErrors = &commsTextLogErrors{ ErrUniqueTextLogPkey: &UniqueConstraintError{ schema: "comms", table: "text_log", - columns: []string{"destination", "source", "type"}, + columns: []string{"id"}, s: "text_log_pkey", }, } diff --git a/db/dberrors/organization.bob.go b/db/dberrors/organization.bob.go index fee0219d..d3199621 100644 --- a/db/dberrors/organization.bob.go +++ b/db/dberrors/organization.bob.go @@ -18,6 +18,13 @@ var OrganizationErrors = &organizationErrors{ s: "organization_import_district_gid_key", }, + ErrUniqueOrganizationSlugKey: &UniqueConstraintError{ + schema: "", + table: "organization", + columns: []string{"slug"}, + s: "organization_slug_key", + }, + ErrUniqueOrganizationWebsiteKey: &UniqueConstraintError{ schema: "", table: "organization", @@ -31,5 +38,7 @@ type organizationErrors struct { ErrUniqueOrganizationImportDistrictGidKey *UniqueConstraintError + ErrUniqueOrganizationSlugKey *UniqueConstraintError + ErrUniqueOrganizationWebsiteKey *UniqueConstraintError } diff --git a/db/dbinfo/comms.text_log.bob.go b/db/dbinfo/comms.text_log.bob.go index 308ca842..7447d67d 100644 --- a/db/dbinfo/comms.text_log.bob.go +++ b/db/dbinfo/comms.text_log.bob.go @@ -15,6 +15,15 @@ var CommsTextLogs = Table[ Schema: "comms", Name: "text_log", Columns: commsTextLogColumns{ + Content: column{ + Name: "content", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, Created: column{ Name: "created", DBType: "timestamp without time zone", @@ -33,18 +42,27 @@ var CommsTextLogs = Table[ Generated: false, AutoIncr: false, }, - Source: column{ - Name: "source", - DBType: "text", + ID: column{ + Name: "id", + DBType: "integer", + Default: "nextval('comms.text_log_id_seq'::regclass)", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Origin: column{ + Name: "origin", + DBType: "comms.textorigin", Default: "", Comment: "", Nullable: false, Generated: false, AutoIncr: false, }, - Type: column{ - Name: "type", - DBType: "comms.messagetypetext", + Source: column{ + Name: "source", + DBType: "text", Default: "", Comment: "", Nullable: false, @@ -58,24 +76,14 @@ var CommsTextLogs = Table[ Name: "text_log_pkey", Columns: []indexColumn{ { - Name: "destination", - Desc: null.FromCond(false, true), - IsExpression: false, - }, - { - Name: "source", - Desc: null.FromCond(false, true), - IsExpression: false, - }, - { - Name: "type", + Name: "id", Desc: null.FromCond(false, true), IsExpression: false, }, }, Unique: true, Comment: "", - NullsFirst: []bool{false, false, false}, + NullsFirst: []bool{false}, NullsDistinct: false, Where: "", Include: []string{}, @@ -83,7 +91,7 @@ var CommsTextLogs = Table[ }, PrimaryKey: &constraint{ Name: "text_log_pkey", - Columns: []string{"destination", "source", "type"}, + Columns: []string{"id"}, Comment: "", }, ForeignKeys: commsTextLogForeignKeys{ @@ -111,15 +119,17 @@ var CommsTextLogs = Table[ } type commsTextLogColumns struct { + Content column Created column Destination column + ID column + Origin column Source column - Type column } func (c commsTextLogColumns) AsSlice() []column { return []column{ - c.Created, c.Destination, c.Source, c.Type, + c.Content, c.Created, c.Destination, c.ID, c.Origin, c.Source, } } diff --git a/db/dbinfo/organization.bob.go b/db/dbinfo/organization.bob.go index 23be6de2..da9cf320 100644 --- a/db/dbinfo/organization.bob.go +++ b/db/dbinfo/organization.bob.go @@ -132,6 +132,23 @@ var Organizations = Table[ Where: "", Include: []string{}, }, + OrganizationSlugKey: index{ + Type: "btree", + Name: "organization_slug_key", + Columns: []indexColumn{ + { + Name: "slug", + Desc: null.FromCond(false, true), + IsExpression: false, + }, + }, + Unique: true, + Comment: "", + NullsFirst: []bool{false}, + NullsDistinct: false, + Where: "", + Include: []string{}, + }, OrganizationWebsiteKey: index{ Type: "btree", Name: "organization_website_key", @@ -172,6 +189,11 @@ var Organizations = Table[ Columns: []string{"import_district_gid"}, Comment: "", }, + OrganizationSlugKey: constraint{ + Name: "organization_slug_key", + Columns: []string{"slug"}, + Comment: "", + }, OrganizationWebsiteKey: constraint{ Name: "organization_website_key", Columns: []string{"website"}, @@ -203,12 +225,13 @@ func (c organizationColumns) AsSlice() []column { type organizationIndexes struct { OrganizationPkey index OrganizationImportDistrictGidKey index + OrganizationSlugKey index OrganizationWebsiteKey index } func (i organizationIndexes) AsSlice() []index { return []index{ - i.OrganizationPkey, i.OrganizationImportDistrictGidKey, i.OrganizationWebsiteKey, + i.OrganizationPkey, i.OrganizationImportDistrictGidKey, i.OrganizationSlugKey, i.OrganizationWebsiteKey, } } @@ -224,12 +247,13 @@ func (f organizationForeignKeys) AsSlice() []foreignKey { type organizationUniques struct { OrganizationImportDistrictGidKey constraint + OrganizationSlugKey constraint OrganizationWebsiteKey constraint } func (u organizationUniques) AsSlice() []constraint { return []constraint{ - u.OrganizationImportDistrictGidKey, u.OrganizationWebsiteKey, + u.OrganizationImportDistrictGidKey, u.OrganizationSlugKey, u.OrganizationWebsiteKey, } } diff --git a/db/enums/enums.bob.go b/db/enums/enums.bob.go index 373cd454..2cccb414 100644 --- a/db/enums/enums.bob.go +++ b/db/enums/enums.bob.go @@ -272,35 +272,32 @@ func (e *CommsMessagetypeemail) Scan(value any) error { return nil } -// Enum values for CommsMessagetypetext +// Enum values for CommsTextorigin const ( - CommsMessagetypetextInitialContact CommsMessagetypetext = "initial-contact" - CommsMessagetypetextReportSubscriptionConfirmation CommsMessagetypetext = "report-subscription-confirmation" - CommsMessagetypetextReportStatusScheduled CommsMessagetypetext = "report-status-scheduled" - CommsMessagetypetextReportStatusComplete CommsMessagetypetext = "report-status-complete" + CommsTextoriginDistrict CommsTextorigin = "district" + CommsTextoriginLLM CommsTextorigin = "llm" + CommsTextoriginWebsiteAction CommsTextorigin = "website-action" ) -func AllCommsMessagetypetext() []CommsMessagetypetext { - return []CommsMessagetypetext{ - CommsMessagetypetextInitialContact, - CommsMessagetypetextReportSubscriptionConfirmation, - CommsMessagetypetextReportStatusScheduled, - CommsMessagetypetextReportStatusComplete, +func AllCommsTextorigin() []CommsTextorigin { + return []CommsTextorigin{ + CommsTextoriginDistrict, + CommsTextoriginLLM, + CommsTextoriginWebsiteAction, } } -type CommsMessagetypetext string +type CommsTextorigin string -func (e CommsMessagetypetext) String() string { +func (e CommsTextorigin) String() string { return string(e) } -func (e CommsMessagetypetext) Valid() bool { +func (e CommsTextorigin) Valid() bool { switch e { - case CommsMessagetypetextInitialContact, - CommsMessagetypetextReportSubscriptionConfirmation, - CommsMessagetypetextReportStatusScheduled, - CommsMessagetypetextReportStatusComplete: + case CommsTextoriginDistrict, + CommsTextoriginLLM, + CommsTextoriginWebsiteAction: return true default: return false @@ -308,44 +305,44 @@ func (e CommsMessagetypetext) Valid() bool { } // useful when testing in other packages -func (e CommsMessagetypetext) All() []CommsMessagetypetext { - return AllCommsMessagetypetext() +func (e CommsTextorigin) All() []CommsTextorigin { + return AllCommsTextorigin() } -func (e CommsMessagetypetext) MarshalText() ([]byte, error) { +func (e CommsTextorigin) MarshalText() ([]byte, error) { return []byte(e), nil } -func (e *CommsMessagetypetext) UnmarshalText(text []byte) error { +func (e *CommsTextorigin) UnmarshalText(text []byte) error { return e.Scan(text) } -func (e CommsMessagetypetext) MarshalBinary() ([]byte, error) { +func (e CommsTextorigin) MarshalBinary() ([]byte, error) { return []byte(e), nil } -func (e *CommsMessagetypetext) UnmarshalBinary(data []byte) error { +func (e *CommsTextorigin) UnmarshalBinary(data []byte) error { return e.Scan(data) } -func (e CommsMessagetypetext) Value() (driver.Value, error) { +func (e CommsTextorigin) Value() (driver.Value, error) { return string(e), nil } -func (e *CommsMessagetypetext) Scan(value any) error { +func (e *CommsTextorigin) Scan(value any) error { switch x := value.(type) { case string: - *e = CommsMessagetypetext(x) + *e = CommsTextorigin(x) case []byte: - *e = CommsMessagetypetext(x) + *e = CommsTextorigin(x) case nil: - return fmt.Errorf("cannot nil into CommsMessagetypetext") + return fmt.Errorf("cannot nil into CommsTextorigin") default: return fmt.Errorf("cannot scan type %T: %v", value, value) } if !e.Valid() { - return fmt.Errorf("invalid CommsMessagetypetext value: %s", *e) + return fmt.Errorf("invalid CommsTextorigin value: %s", *e) } return nil diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index 00bfe2bc..ab527cba 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -324,10 +324,12 @@ func (f *Factory) NewCommsTextLogWithContext(ctx context.Context, mods ...CommsT func (f *Factory) FromExistingCommsTextLog(m *models.CommsTextLog) *CommsTextLogTemplate { o := &CommsTextLogTemplate{f: f, alreadyPersisted: true} + o.Content = func() string { return m.Content } o.Created = func() time.Time { return m.Created } o.Destination = func() string { return m.Destination } + o.ID = func() int32 { return m.ID } + o.Origin = func() enums.CommsTextorigin { return m.Origin } o.Source = func() string { return m.Source } - o.Type = func() enums.CommsMessagetypetext { return m.Type } ctx := context.Background() if m.R.DestinationPhone != nil { diff --git a/db/factory/bobfactory_random.bob.go b/db/factory/bobfactory_random.bob.go index dfa5ebc5..23a5b12f 100644 --- a/db/factory/bobfactory_random.bob.go +++ b/db/factory/bobfactory_random.bob.go @@ -101,12 +101,12 @@ func random_enums_CommsMessagetypeemail(f *faker.Faker, limits ...string) enums. return all[f.IntBetween(0, len(all)-1)] } -func random_enums_CommsMessagetypetext(f *faker.Faker, limits ...string) enums.CommsMessagetypetext { +func random_enums_CommsTextorigin(f *faker.Faker, limits ...string) enums.CommsTextorigin { if f == nil { f = &defaultFaker } - var e enums.CommsMessagetypetext + var e enums.CommsTextorigin all := e.All() return all[f.IntBetween(0, len(all)-1)] } diff --git a/db/factory/comms.text_log.bob.go b/db/factory/comms.text_log.bob.go index 91d5a2f3..6b1cd6e0 100644 --- a/db/factory/comms.text_log.bob.go +++ b/db/factory/comms.text_log.bob.go @@ -36,10 +36,12 @@ func (mods CommsTextLogModSlice) Apply(ctx context.Context, n *CommsTextLogTempl // CommsTextLogTemplate is an object representing the database table. // all columns are optional and should be set by mods type CommsTextLogTemplate struct { + Content func() string Created func() time.Time Destination func() string + ID func() int32 + Origin func() enums.CommsTextorigin Source func() string - Type func() enums.CommsMessagetypetext r commsTextLogR f *Factory @@ -89,6 +91,10 @@ func (t CommsTextLogTemplate) setModelRels(o *models.CommsTextLog) { func (o CommsTextLogTemplate) BuildSetter() *models.CommsTextLogSetter { m := &models.CommsTextLogSetter{} + if o.Content != nil { + val := o.Content() + m.Content = omit.From(val) + } if o.Created != nil { val := o.Created() m.Created = omit.From(val) @@ -97,14 +103,18 @@ func (o CommsTextLogTemplate) BuildSetter() *models.CommsTextLogSetter { val := o.Destination() m.Destination = omit.From(val) } + if o.ID != nil { + val := o.ID() + m.ID = omit.From(val) + } + if o.Origin != nil { + val := o.Origin() + m.Origin = omit.From(val) + } if o.Source != nil { val := o.Source() m.Source = omit.From(val) } - if o.Type != nil { - val := o.Type() - m.Type = omit.From(val) - } return m } @@ -127,18 +137,24 @@ func (o CommsTextLogTemplate) BuildManySetter(number int) []*models.CommsTextLog func (o CommsTextLogTemplate) Build() *models.CommsTextLog { m := &models.CommsTextLog{} + if o.Content != nil { + m.Content = o.Content() + } if o.Created != nil { m.Created = o.Created() } if o.Destination != nil { m.Destination = o.Destination() } + if o.ID != nil { + m.ID = o.ID() + } + if o.Origin != nil { + m.Origin = o.Origin() + } if o.Source != nil { m.Source = o.Source() } - if o.Type != nil { - m.Type = o.Type() - } o.setModelRels(m) @@ -159,6 +175,10 @@ func (o CommsTextLogTemplate) BuildMany(number int) models.CommsTextLogSlice { } func ensureCreatableCommsTextLog(m *models.CommsTextLogSetter) { + if !(m.Content.IsValue()) { + val := random_string(nil) + m.Content = omit.From(val) + } if !(m.Created.IsValue()) { val := random_time_Time(nil) m.Created = omit.From(val) @@ -167,14 +187,14 @@ func ensureCreatableCommsTextLog(m *models.CommsTextLogSetter) { val := random_string(nil) m.Destination = omit.From(val) } + if !(m.Origin.IsValue()) { + val := random_enums_CommsTextorigin(nil) + m.Origin = omit.From(val) + } if !(m.Source.IsValue()) { val := random_string(nil) m.Source = omit.From(val) } - if !(m.Type.IsValue()) { - val := random_enums_CommsMessagetypetext(nil) - m.Type = omit.From(val) - } } // insertOptRels creates and inserts any optional the relationships on *models.CommsTextLog @@ -312,13 +332,46 @@ type commsTextLogMods struct{} func (m commsTextLogMods) RandomizeAllColumns(f *faker.Faker) CommsTextLogMod { return CommsTextLogModSlice{ + CommsTextLogMods.RandomContent(f), CommsTextLogMods.RandomCreated(f), CommsTextLogMods.RandomDestination(f), + CommsTextLogMods.RandomID(f), + CommsTextLogMods.RandomOrigin(f), CommsTextLogMods.RandomSource(f), - CommsTextLogMods.RandomType(f), } } +// Set the model columns to this value +func (m commsTextLogMods) Content(val string) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.Content = func() string { return val } + }) +} + +// Set the Column from the function +func (m commsTextLogMods) ContentFunc(f func() string) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.Content = f + }) +} + +// Clear any values for the column +func (m commsTextLogMods) UnsetContent() CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.Content = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextLogMods) RandomContent(f *faker.Faker) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.Content = func() string { + return random_string(f) + } + }) +} + // Set the model columns to this value func (m commsTextLogMods) Created(val time.Time) CommsTextLogMod { return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { @@ -381,6 +434,68 @@ func (m commsTextLogMods) RandomDestination(f *faker.Faker) CommsTextLogMod { }) } +// Set the model columns to this value +func (m commsTextLogMods) ID(val int32) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.ID = func() int32 { return val } + }) +} + +// Set the Column from the function +func (m commsTextLogMods) IDFunc(f func() int32) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.ID = f + }) +} + +// Clear any values for the column +func (m commsTextLogMods) UnsetID() CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.ID = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextLogMods) RandomID(f *faker.Faker) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.ID = func() int32 { + return random_int32(f) + } + }) +} + +// Set the model columns to this value +func (m commsTextLogMods) Origin(val enums.CommsTextorigin) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.Origin = func() enums.CommsTextorigin { return val } + }) +} + +// Set the Column from the function +func (m commsTextLogMods) OriginFunc(f func() enums.CommsTextorigin) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.Origin = f + }) +} + +// Clear any values for the column +func (m commsTextLogMods) UnsetOrigin() CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.Origin = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextLogMods) RandomOrigin(f *faker.Faker) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.Origin = func() enums.CommsTextorigin { + return random_enums_CommsTextorigin(f) + } + }) +} + // Set the model columns to this value func (m commsTextLogMods) Source(val string) CommsTextLogMod { return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { @@ -412,37 +527,6 @@ func (m commsTextLogMods) RandomSource(f *faker.Faker) CommsTextLogMod { }) } -// Set the model columns to this value -func (m commsTextLogMods) Type(val enums.CommsMessagetypetext) CommsTextLogMod { - return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { - o.Type = func() enums.CommsMessagetypetext { return val } - }) -} - -// Set the Column from the function -func (m commsTextLogMods) TypeFunc(f func() enums.CommsMessagetypetext) CommsTextLogMod { - return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { - o.Type = f - }) -} - -// Clear any values for the column -func (m commsTextLogMods) UnsetType() CommsTextLogMod { - return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { - o.Type = nil - }) -} - -// Generates a random value for the column using the given faker -// if faker is nil, a default faker is used -func (m commsTextLogMods) RandomType(f *faker.Faker) CommsTextLogMod { - return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { - o.Type = func() enums.CommsMessagetypetext { - return random_enums_CommsMessagetypetext(f) - } - }) -} - func (m commsTextLogMods) WithParentsCascading() CommsTextLogMod { return CommsTextLogModFunc(func(ctx context.Context, o *CommsTextLogTemplate) { if isDone, _ := commsTextLogWithParentsCascadingCtx.Value(ctx); isDone { diff --git a/db/migrations/00041_text_log_overhaul.sql b/db/migrations/00041_text_log_overhaul.sql new file mode 100644 index 00000000..4e33d846 --- /dev/null +++ b/db/migrations/00041_text_log_overhaul.sql @@ -0,0 +1,34 @@ +-- +goose Up +DROP TABLE comms.text_log; +DROP TYPE comms.MessageTypeText; +CREATE TYPE comms.TextOrigin AS ENUM ( + 'district', + 'llm', + 'website-action' +); +CREATE TABLE comms.text_log ( + content TEXT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + destination TEXT NOT NULL REFERENCES comms.phone(e164), + id SERIAL, + origin comms.TextOrigin NOT NULL, + source TEXT NOT NULL REFERENCES comms.phone(e164), + PRIMARY KEY(id) +); + +-- +goose Down +DROP TABLE comms.text_log; +DROP TYPE comms.TextOrigin; +CREATE TYPE comms.MessageTypeText AS ENUM ( + 'initial-contact', + 'report-subscription-confirmation', + 'report-status-scheduled', + 'report-status-complete' +); +CREATE TABLE comms.text_log ( + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + destination TEXT NOT NULL REFERENCES comms.phone(e164), + source TEXT NOT NULL REFERENCES comms.phone(e164), + type comms.MessageTypeText NOT NULL, + PRIMARY KEY (destination, source, type) +); diff --git a/db/models/comms.text_log.bob.go b/db/models/comms.text_log.bob.go index d717c547..fc1054d3 100644 --- a/db/models/comms.text_log.bob.go +++ b/db/models/comms.text_log.bob.go @@ -25,10 +25,12 @@ import ( // CommsTextLog is an object representing the database table. type CommsTextLog struct { - Created time.Time `db:"created" ` - Destination string `db:"destination,pk" ` - Source string `db:"source,pk" ` - Type enums.CommsMessagetypetext `db:"type,pk" ` + Content string `db:"content" ` + Created time.Time `db:"created" ` + Destination string `db:"destination" ` + ID int32 `db:"id,pk" ` + Origin enums.CommsTextorigin `db:"origin" ` + Source string `db:"source" ` R commsTextLogR `db:"-" ` } @@ -52,23 +54,27 @@ type commsTextLogR struct { func buildCommsTextLogColumns(alias string) commsTextLogColumns { return commsTextLogColumns{ ColumnsExpr: expr.NewColumnsExpr( - "created", "destination", "source", "type", + "content", "created", "destination", "id", "origin", "source", ).WithParent("comms.text_log"), tableAlias: alias, + Content: psql.Quote(alias, "content"), Created: psql.Quote(alias, "created"), Destination: psql.Quote(alias, "destination"), + ID: psql.Quote(alias, "id"), + Origin: psql.Quote(alias, "origin"), Source: psql.Quote(alias, "source"), - Type: psql.Quote(alias, "type"), } } type commsTextLogColumns struct { expr.ColumnsExpr tableAlias string + Content psql.Expression Created psql.Expression Destination psql.Expression + ID psql.Expression + Origin psql.Expression Source psql.Expression - Type psql.Expression } func (c commsTextLogColumns) Alias() string { @@ -83,42 +89,56 @@ func (commsTextLogColumns) AliasedAs(alias string) commsTextLogColumns { // All values are optional, and do not have to be set // Generated columns are not included type CommsTextLogSetter struct { - Created omit.Val[time.Time] `db:"created" ` - Destination omit.Val[string] `db:"destination,pk" ` - Source omit.Val[string] `db:"source,pk" ` - Type omit.Val[enums.CommsMessagetypetext] `db:"type,pk" ` + Content omit.Val[string] `db:"content" ` + Created omit.Val[time.Time] `db:"created" ` + Destination omit.Val[string] `db:"destination" ` + ID omit.Val[int32] `db:"id,pk" ` + Origin omit.Val[enums.CommsTextorigin] `db:"origin" ` + Source omit.Val[string] `db:"source" ` } func (s CommsTextLogSetter) SetColumns() []string { - vals := make([]string, 0, 4) + vals := make([]string, 0, 6) + if s.Content.IsValue() { + vals = append(vals, "content") + } if s.Created.IsValue() { vals = append(vals, "created") } if s.Destination.IsValue() { vals = append(vals, "destination") } + if s.ID.IsValue() { + vals = append(vals, "id") + } + if s.Origin.IsValue() { + vals = append(vals, "origin") + } if s.Source.IsValue() { vals = append(vals, "source") } - if s.Type.IsValue() { - vals = append(vals, "type") - } return vals } func (s CommsTextLogSetter) Overwrite(t *CommsTextLog) { + if s.Content.IsValue() { + t.Content = s.Content.MustGet() + } if s.Created.IsValue() { t.Created = s.Created.MustGet() } if s.Destination.IsValue() { t.Destination = s.Destination.MustGet() } + if s.ID.IsValue() { + t.ID = s.ID.MustGet() + } + if s.Origin.IsValue() { + t.Origin = s.Origin.MustGet() + } if s.Source.IsValue() { t.Source = s.Source.MustGet() } - if s.Type.IsValue() { - t.Type = s.Type.MustGet() - } } func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { @@ -127,31 +147,43 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { }) q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { - vals := make([]bob.Expression, 4) - if s.Created.IsValue() { - vals[0] = psql.Arg(s.Created.MustGet()) + vals := make([]bob.Expression, 6) + if s.Content.IsValue() { + vals[0] = psql.Arg(s.Content.MustGet()) } else { vals[0] = psql.Raw("DEFAULT") } - if s.Destination.IsValue() { - vals[1] = psql.Arg(s.Destination.MustGet()) + if s.Created.IsValue() { + vals[1] = psql.Arg(s.Created.MustGet()) } else { vals[1] = psql.Raw("DEFAULT") } - if s.Source.IsValue() { - vals[2] = psql.Arg(s.Source.MustGet()) + if s.Destination.IsValue() { + vals[2] = psql.Arg(s.Destination.MustGet()) } else { vals[2] = psql.Raw("DEFAULT") } - if s.Type.IsValue() { - vals[3] = psql.Arg(s.Type.MustGet()) + if s.ID.IsValue() { + vals[3] = psql.Arg(s.ID.MustGet()) } else { vals[3] = psql.Raw("DEFAULT") } + if s.Origin.IsValue() { + vals[4] = psql.Arg(s.Origin.MustGet()) + } else { + vals[4] = psql.Raw("DEFAULT") + } + + if s.Source.IsValue() { + vals[5] = psql.Arg(s.Source.MustGet()) + } else { + vals[5] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -161,7 +193,14 @@ func (s CommsTextLogSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 4) + exprs := make([]bob.Expression, 0, 6) + + if s.Content.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "content")...), + psql.Arg(s.Content), + }}) + } if s.Created.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -177,6 +216,20 @@ func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if s.ID.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "id")...), + psql.Arg(s.ID), + }}) + } + + if s.Origin.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "origin")...), + psql.Arg(s.Origin), + }}) + } + if s.Source.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "source")...), @@ -184,41 +237,28 @@ func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { }}) } - if s.Type.IsValue() { - exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ - psql.Quote(append(prefix, "type")...), - psql.Arg(s.Type), - }}) - } - return exprs } // FindCommsTextLog retrieves a single record by primary key // If cols is empty Find will return all columns. -func FindCommsTextLog(ctx context.Context, exec bob.Executor, DestinationPK string, SourcePK string, TypePK enums.CommsMessagetypetext, cols ...string) (*CommsTextLog, error) { +func FindCommsTextLog(ctx context.Context, exec bob.Executor, IDPK int32, cols ...string) (*CommsTextLog, error) { if len(cols) == 0 { return CommsTextLogs.Query( - sm.Where(CommsTextLogs.Columns.Destination.EQ(psql.Arg(DestinationPK))), - sm.Where(CommsTextLogs.Columns.Source.EQ(psql.Arg(SourcePK))), - sm.Where(CommsTextLogs.Columns.Type.EQ(psql.Arg(TypePK))), + sm.Where(CommsTextLogs.Columns.ID.EQ(psql.Arg(IDPK))), ).One(ctx, exec) } return CommsTextLogs.Query( - sm.Where(CommsTextLogs.Columns.Destination.EQ(psql.Arg(DestinationPK))), - sm.Where(CommsTextLogs.Columns.Source.EQ(psql.Arg(SourcePK))), - sm.Where(CommsTextLogs.Columns.Type.EQ(psql.Arg(TypePK))), + sm.Where(CommsTextLogs.Columns.ID.EQ(psql.Arg(IDPK))), sm.Columns(CommsTextLogs.Columns.Only(cols...)), ).One(ctx, exec) } // CommsTextLogExists checks the presence of a single record by primary key -func CommsTextLogExists(ctx context.Context, exec bob.Executor, DestinationPK string, SourcePK string, TypePK enums.CommsMessagetypetext) (bool, error) { +func CommsTextLogExists(ctx context.Context, exec bob.Executor, IDPK int32) (bool, error) { return CommsTextLogs.Query( - sm.Where(CommsTextLogs.Columns.Destination.EQ(psql.Arg(DestinationPK))), - sm.Where(CommsTextLogs.Columns.Source.EQ(psql.Arg(SourcePK))), - sm.Where(CommsTextLogs.Columns.Type.EQ(psql.Arg(TypePK))), + sm.Where(CommsTextLogs.Columns.ID.EQ(psql.Arg(IDPK))), ).Exists(ctx, exec) } @@ -242,15 +282,11 @@ func (o *CommsTextLog) AfterQueryHook(ctx context.Context, exec bob.Executor, qu // primaryKeyVals returns the primary key values of the CommsTextLog func (o *CommsTextLog) primaryKeyVals() bob.Expression { - return psql.ArgGroup( - o.Destination, - o.Source, - o.Type, - ) + return psql.Arg(o.ID) } func (o *CommsTextLog) pkEQ() dialect.Expression { - return psql.Group(psql.Quote("comms.text_log", "destination"), psql.Quote("comms.text_log", "source"), psql.Quote("comms.text_log", "type")).EQ(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + return psql.Quote("comms.text_log", "id").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { return o.primaryKeyVals().WriteSQL(ctx, w, d, start) })) } @@ -277,9 +313,7 @@ func (o *CommsTextLog) Delete(ctx context.Context, exec bob.Executor) error { // Reload refreshes the CommsTextLog using the executor func (o *CommsTextLog) Reload(ctx context.Context, exec bob.Executor) error { o2, err := CommsTextLogs.Query( - sm.Where(CommsTextLogs.Columns.Destination.EQ(psql.Arg(o.Destination))), - sm.Where(CommsTextLogs.Columns.Source.EQ(psql.Arg(o.Source))), - sm.Where(CommsTextLogs.Columns.Type.EQ(psql.Arg(o.Type))), + sm.Where(CommsTextLogs.Columns.ID.EQ(psql.Arg(o.ID))), ).One(ctx, exec) if err != nil { return err @@ -313,7 +347,7 @@ func (o CommsTextLogSlice) pkIN() dialect.Expression { return psql.Raw("NULL") } - return psql.Group(psql.Quote("comms.text_log", "destination"), psql.Quote("comms.text_log", "source"), psql.Quote("comms.text_log", "type")).In(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + return psql.Quote("comms.text_log", "id").In(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { pkPairs := make([]bob.Expression, len(o)) for i, row := range o { pkPairs[i] = row.primaryKeyVals() @@ -328,13 +362,7 @@ func (o CommsTextLogSlice) pkIN() dialect.Expression { func (o CommsTextLogSlice) copyMatchingRows(from ...*CommsTextLog) { for i, old := range o { for _, new := range from { - if new.Destination != old.Destination { - continue - } - if new.Source != old.Source { - continue - } - if new.Type != old.Type { + if new.ID != old.ID { continue } new.R = old.R @@ -580,10 +608,12 @@ func (commsTextLog0 *CommsTextLog) AttachSourcePhone(ctx context.Context, exec b } type commsTextLogWhere[Q psql.Filterable] struct { + Content psql.WhereMod[Q, string] Created psql.WhereMod[Q, time.Time] Destination psql.WhereMod[Q, string] + ID psql.WhereMod[Q, int32] + Origin psql.WhereMod[Q, enums.CommsTextorigin] Source psql.WhereMod[Q, string] - Type psql.WhereMod[Q, enums.CommsMessagetypetext] } func (commsTextLogWhere[Q]) AliasedAs(alias string) commsTextLogWhere[Q] { @@ -592,10 +622,12 @@ func (commsTextLogWhere[Q]) AliasedAs(alias string) commsTextLogWhere[Q] { func buildCommsTextLogWhere[Q psql.Filterable](cols commsTextLogColumns) commsTextLogWhere[Q] { return commsTextLogWhere[Q]{ + Content: psql.Where[Q, string](cols.Content), Created: psql.Where[Q, time.Time](cols.Created), Destination: psql.Where[Q, string](cols.Destination), + ID: psql.Where[Q, int32](cols.ID), + Origin: psql.Where[Q, enums.CommsTextorigin](cols.Origin), Source: psql.Where[Q, string](cols.Source), - Type: psql.Where[Q, enums.CommsMessagetypetext](cols.Type), } } diff --git a/public-report/quick.go b/public-report/quick.go index cfb27a0b..beb95308 100644 --- a/public-report/quick.go +++ b/public-report/quick.go @@ -8,7 +8,7 @@ import ( "time" "github.com/Gleipnir-Technology/nidus-sync/background" - "github.com/Gleipnir-Technology/nidus-sync/comms" + "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" @@ -241,7 +241,7 @@ func postRegisterNotifications(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, fmt.Sprintf("/quick-submit-complete?report=%s", report_id), http.StatusFound) return } - phone, err := comms.ParsePhoneNumber(phone_str) + phone, err := text.ParsePhoneNumber(phone_str) result, err := psql.Update( um.Table("publicreport.quick"), um.SetCol("reporter_email").ToArg(email), diff --git a/public-report/template/mock/nuisance.html b/public-report/template/mock/nuisance.html index 0cb6e57b..85f3c6fb 100644 --- a/public-report/template/mock/nuisance.html +++ b/public-report/template/mock/nuisance.html @@ -385,7 +385,7 @@ document.addEventListener('DOMContentLoaded', function() {
-
diff --git a/public-report/template/mock/water.html b/public-report/template/mock/water.html index dd5d244d..d37ebf2e 100644 --- a/public-report/template/mock/water.html +++ b/public-report/template/mock/water.html @@ -347,7 +347,7 @@ function displaySelectedCoordinates(lngLat) {
-
From 82081b960997d85ebd6492b344b5d1d62d6888f4 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sun, 25 Jan 2026 19:36:56 +0000 Subject: [PATCH 0134/1513] Add API signin URL That was we can have much more specific failure modes for API clients --- api/routes.go | 1 + api/signin.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ auth/auth.go | 17 ++++++++++++++++- 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 api/signin.go diff --git a/api/routes.go b/api/routes.go index 80726f40..8db2879b 100644 --- a/api/routes.go +++ b/api/routes.go @@ -22,6 +22,7 @@ func AddRoutes(r chi.Router) { // Unauthenticated endpoints r.Get("/district", apiGetDistrict) r.Get("/district/{slug}/logo", apiGetDistrictLogo) + r.Post("/signin", postSignin) r.Post("/twilio/message", twilioMessagePost) r.Post("/twilio/status", twilioStatusPost) r.Post("/twilio/text", twilioTextPost) diff --git a/api/signin.go b/api/signin.go new file mode 100644 index 00000000..d182c897 --- /dev/null +++ b/api/signin.go @@ -0,0 +1,45 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + + "github.com/Gleipnir-Technology/nidus-sync/auth" + "github.com/go-chi/render" + "github.com/rs/zerolog/log" +) + +func postSignin(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + render.Render(w, r, errRender(fmt.Errorf("Failed to parse POST form: %w", err))) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + + if password == "" || username == "" { + w.Header().Set("WWW-Authenticate-Error", "no-credentials") + http.Error(w, "invalid-credentials", http.StatusUnauthorized) + return + } + log.Info().Str("username", username).Msg("API Signin") + _, err := auth.SigninUser(r, username, password) + if err != nil { + if errors.Is(err, auth.InvalidCredentials{}) { + w.Header().Set("WWW-Authenticate-Error", "invalid-credentials") + http.Error(w, "invalid-credentials", http.StatusUnauthorized) + return + } + if errors.Is(err, auth.InvalidUsername{}) { + w.Header().Set("WWW-Authenticate-Error", "invalid-credentials") + http.Error(w, "invalid-credentials", http.StatusUnauthorized) + return + } + http.Error(w, "signin-server-error", http.StatusInternalServerError) + return + } + + http.Error(w, "", http.StatusAccepted) +} diff --git a/auth/auth.go b/auth/auth.go index 7548ecca..d5245414 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "strconv" + "strings" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" @@ -188,6 +189,18 @@ func hashPassword(password string) (string, error) { return string(bytes), err } +func redact(s string) string { + if len(s) <= 4 { + return s + } + + first_two := s[:2] + last_two := s[len(s)-2:] + middle_length := len(s) - 4 + + return first_two + strings.Repeat("*", middle_length) + last_two +} + func validatePassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil @@ -198,17 +211,18 @@ func validateUser(ctx context.Context, username string, password string) (*model if err != nil { return nil, fmt.Errorf("Failed to hash password: %w", err) } - log.Info().Str("username", username).Str("password", password).Str("hash", passwordHash).Msg("Validating user") result, err := sql.UserByUsername(username).All(ctx, db.PGInstance.BobDB) if err != nil { return nil, fmt.Errorf("Failed to query for user: %w", err) } switch len(result) { case 0: + log.Info().Str("username", username).Str("password", redact(password)).Msg("Invalid username") return nil, InvalidUsername{} case 1: row := result[0] if !validatePassword(password, row.PasswordHash) { + log.Info().Str("username", username).Str("password", redact(password)).Str("hash", passwordHash).Msg("Invalid password for user") return nil, InvalidCredentials{} } user := models.User{ @@ -223,6 +237,7 @@ func validateUser(ctx context.Context, username string, password string) (*model OrganizationID: row.OrganizationID, Username: row.Username, } + log.Info().Str("username", username).Msg("Validated user") return &user, nil default: return nil, errors.New("More than one matching row, this should be impossible.") From ab105e16e8c54a96a5f089f8445614a7d4e6c802 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Sun, 25 Jan 2026 21:18:39 +0000 Subject: [PATCH 0135/1513] Remove some user session logs that we don't need --- auth/auth.go | 2 -- sync/signin.go | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index d5245414..688ec2c6 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -46,7 +46,6 @@ func AddUserSession(r *http.Request, user *models.User) { id := strconv.Itoa(int(user.ID)) sessionManager.Put(r.Context(), "user_id", id) sessionManager.Put(r.Context(), "username", user.Username) - log.Info().Str("username", user.Username).Str("user_id", id).Msg("Created new user session") } func GetAuthenticatedUser(r *http.Request) (*models.User, error) { @@ -58,7 +57,6 @@ func GetAuthenticatedUser(r *http.Request) (*models.User, error) { return nil, fmt.Errorf("Failed to convert user_id to int: %w", err) } username := sessionManager.GetString(r.Context(), "username") - log.Info().Int("user_id", user_id).Str("username", username).Msg("Current session info") if user_id > 0 && username != "" { return findUser(r.Context(), user_id) } diff --git a/sync/signin.go b/sync/signin.go index b1f9b63f..6b72d011 100644 --- a/sync/signin.go +++ b/sync/signin.go @@ -39,7 +39,7 @@ func postSignin(w http.ResponseWriter, r *http.Request) { username := r.FormValue("username") password := r.FormValue("password") - log.Info().Str("username", username).Msg("Signin") + log.Info().Str("username", username).Msg("HTML Signin") _, err := auth.SigninUser(r, username, password) if err != nil { From adc99e8871e992981145ce43e9624259f66dba41 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 16:10:30 +0000 Subject: [PATCH 0136/1513] Add ability to delay text message sending --- background/text.go | 2 +- comms/text/db.go | 33 ++ comms/text/initial.go | 31 + comms/text/report-subscription.go | 19 +- comms/text/text.go | 16 +- config/config.go | 4 +- db/dberrors/comms.text_job.bob.go | 17 + db/dbinfo/comms.text_job.bob.go | 147 +++++ db/dbinfo/comms.text_log.bob.go | 14 +- db/enums/enums.bob.go | 70 +++ db/factory/bobfactory_context.bob.go | 5 + db/factory/bobfactory_main.bob.go | 46 ++ db/factory/bobfactory_random.bob.go | 10 + db/factory/comms.phone.bob.go | 94 ++- db/factory/comms.text_job.bob.go | 499 ++++++++++++++++ db/factory/comms.text_log.bob.go | 44 ++ db/migrations/00041_text_log_overhaul.sql | 16 + db/models/bob_joins.bob.go | 2 + db/models/bob_loaders.bob.go | 4 + db/models/bob_where.bob.go | 3 + db/models/comms.phone.bob.go | 254 ++++++++ db/models/comms.text_job.bob.go | 679 ++++++++++++++++++++++ db/models/comms.text_log.bob.go | 41 +- main.go | 6 + 24 files changed, 2028 insertions(+), 28 deletions(-) create mode 100644 comms/text/initial.go create mode 100644 db/dberrors/comms.text_job.bob.go create mode 100644 db/dbinfo/comms.text_job.bob.go create mode 100644 db/factory/comms.text_job.bob.go create mode 100644 db/models/comms.text_job.bob.go diff --git a/background/text.go b/background/text.go index 80de3b2b..91308769 100644 --- a/background/text.go +++ b/background/text.go @@ -14,7 +14,7 @@ func ReportSubscriptionConfirmationText(destination text.E164, report_id string) enqueueJobText(text.NewJobReportSubscriptionConfirmation( destination, report_id, - config.RMOPhoneNumber, + config.PhoneNumberReport, )) } diff --git a/comms/text/db.go b/comms/text/db.go index 5b609df9..1795a061 100644 --- a/comms/text/db.go +++ b/comms/text/db.go @@ -10,14 +10,21 @@ import ( "strings" "time" + "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" + "github.com/nyaruka/phonenumbers" "github.com/rs/zerolog/log" "github.com/stephenafamo/bob/types/pgtypes" ) +func StoreSources() error { + ctx := context.TODO() + src := phonenumbers.Format(&config.PhoneNumberReport, phonenumbers.E164) + return ensureInDB(ctx, src) +} func convertToPGData(data map[string]string) pgtypes.HStore { result := pgtypes.HStore{} for k, v := range data { @@ -26,6 +33,21 @@ func convertToPGData(data map[string]string) pgtypes.HStore { return result } +func delayMessage(ctx context.Context, source string, destination string, content string, type_ enums.CommsTextjobtype) error { + job, err := models.CommsTextJobs.Insert(&models.CommsTextJobSetter{ + Content: omit.From(content), + Created: omit.From(time.Now()), + Destination: omit.From(destination), + //ID: + Type: omit.From(type_), + }).One(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to add delayed text job: %w", err) + } + log.Info().Int32("id", job.ID).Msg("Created delayed text job") + return nil +} + func ensureInDB(ctx context.Context, destination string) (err error) { _, err = models.FindCommsPhone(ctx, db.PGInstance.BobDB, destination) if err != nil { @@ -58,6 +80,17 @@ func insertTextLog(ctx context.Context, content string, destination string, sour return err } +func isSubscribed(ctx context.Context, destination string) (bool, error) { + phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, destination) + if err != nil { + if err.Error() == "sql: no rows in result set" { + return false, nil + } + return false, fmt.Errorf("Failed to find phone number %s: %w", destination, err) + } + return phone.IsSubscribed, nil +} + func generatePublicId(t enums.CommsMessagetypeemail, m map[string]string) string { if m == nil || len(m) == 0 { // Return hash of empty string for empty maps diff --git a/comms/text/initial.go b/comms/text/initial.go new file mode 100644 index 00000000..d2bf35a5 --- /dev/null +++ b/comms/text/initial.go @@ -0,0 +1,31 @@ +package text + +import ( + "context" + "fmt" + + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/Gleipnir-Technology/nidus-sync/db/models" +) + +func ensureInitialText(ctx context.Context, src string, dst string) error { + // + origin := enums.CommsTextoriginWebsiteAction + rows, err := models.CommsTextLogs.Query( + models.SelectWhere.CommsTextLogs.Destination.EQ(dst), + models.SelectWhere.CommsTextLogs.IsWelcome.EQ(true), + ).All(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to query text logs: %w", err) + } + if len(rows) > 0 { + return nil + } + content := "Welcome to Report Mosquitoes Online. We received your request and want to confirm text updates. Reply YES to continue. Reply STOP at any time to unsubscribe" + err = sendText(ctx, src, dst, content, origin) + if err != nil { + return fmt.Errorf("Failed to send initial confirmation: %w", err) + } + return nil +} diff --git a/comms/text/report-subscription.go b/comms/text/report-subscription.go index 1dd1f193..092b9ee2 100644 --- a/comms/text/report-subscription.go +++ b/comms/text/report-subscription.go @@ -48,9 +48,24 @@ func sendReportSubscription(ctx context.Context, job Job) error { return fmt.Errorf("job is not for report subscription confirmation") } - err := sendText(ctx, j.src, j.dst, j.content(), enums.CommsTextoriginWebsiteAction) + sub, err := isSubscribed(ctx, job.destination()) if err != nil { - return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + return fmt.Errorf("Failed to check if subscribed: %w", err) + } + if !sub { + err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction) + if err != nil { + return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + } + } else { + err = delayMessage(ctx, j.source(), j.destination(), j.content(), enums.CommsTextjobtypeReportConfirmation) + if err != nil { + return fmt.Errorf("Failed to delay report subscription message: %w", err) + } + err := ensureInitialText(ctx, j.source(), j.destination()) + if err != nil { + return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) + } } return nil } diff --git a/comms/text/text.go b/comms/text/text.go index e0846d2b..415a301d 100644 --- a/comms/text/text.go +++ b/comms/text/text.go @@ -19,14 +19,12 @@ func ParsePhoneNumber(input string) (*E164, error) { return phonenumbers.Parse(input, "US") } -func sendText(ctx context.Context, source E164, destination E164, message string, origin enums.CommsTextorigin) error { - src := phonenumbers.Format(&source, phonenumbers.E164) - dest := phonenumbers.Format(&destination, phonenumbers.E164) - err := ensureInDB(ctx, dest) +func sendText(ctx context.Context, source string, destination string, message string, origin enums.CommsTextorigin) error { + err := ensureInDB(ctx, destination) if err != nil { return fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err) } - err = insertTextLog(ctx, message, dest, src, origin) + err = insertTextLog(ctx, message, destination, source, origin) if err != nil { return fmt.Errorf("Failed to insert text message in the DB: %w", err) } @@ -36,16 +34,16 @@ func sendText(ctx context.Context, source E164, destination E164, message string params.SetMessagingServiceSid(config.TwilioMessagingServiceSID) params.SetBody(message) - params.SetTo(dest) + params.SetTo(destination) resp, err := client.Api.CreateMessage(params) if err != nil { - return fmt.Errorf("Failed to create message to %s: %w", dest, err) + return fmt.Errorf("Failed to create message to %s: %w", destination, err) } else { if resp.Body != nil { - log.Info().Str("dest", dest).Str("body", *resp.Body).Msg("Text message response") + log.Info().Str("dest", destination).Str("body", *resp.Body).Msg("Text message response") } else { - log.Info().Str("dest", dest).Msg("Text message response is nil") + log.Info().Str("dest", destination).Msg("Text message response is nil") } } return nil diff --git a/config/config.go b/config/config.go index 584e5f0c..09b356f2 100644 --- a/config/config.go +++ b/config/config.go @@ -26,7 +26,7 @@ var ( ForwardEmailReportUsername string MapboxToken string PGDSN string - RMOPhoneNumber phonenumbers.PhoneNumber + PhoneNumberReport phonenumbers.PhoneNumber TwilioAuthToken string TwilioAccountSID string TwilioMessagingServiceSID string @@ -135,7 +135,7 @@ func Parse() (err error) { if err != nil { return fmt.Errorf("Failed to parse '%s' as a valid phone number: %w", rmo_phone_number, err) } - RMOPhoneNumber = *p + PhoneNumberReport = *p TwilioAccountSID = os.Getenv("TWILIO_ACCOUNT_SID") if TwilioAccountSID == "" { diff --git a/db/dberrors/comms.text_job.bob.go b/db/dberrors/comms.text_job.bob.go new file mode 100644 index 00000000..cedcca8e --- /dev/null +++ b/db/dberrors/comms.text_job.bob.go @@ -0,0 +1,17 @@ +// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package dberrors + +var CommsTextJobErrors = &commsTextJobErrors{ + ErrUniqueTextJobPkey: &UniqueConstraintError{ + schema: "comms", + table: "text_job", + columns: []string{"id"}, + s: "text_job_pkey", + }, +} + +type commsTextJobErrors struct { + ErrUniqueTextJobPkey *UniqueConstraintError +} diff --git a/db/dbinfo/comms.text_job.bob.go b/db/dbinfo/comms.text_job.bob.go new file mode 100644 index 00000000..aa87ea6f --- /dev/null +++ b/db/dbinfo/comms.text_job.bob.go @@ -0,0 +1,147 @@ +// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package dbinfo + +import "github.com/aarondl/opt/null" + +var CommsTextJobs = Table[ + commsTextJobColumns, + commsTextJobIndexes, + commsTextJobForeignKeys, + commsTextJobUniques, + commsTextJobChecks, +]{ + Schema: "comms", + Name: "text_job", + Columns: commsTextJobColumns{ + Content: column{ + Name: "content", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Created: column{ + Name: "created", + DBType: "timestamp without time zone", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Destination: column{ + Name: "destination", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + ID: column{ + Name: "id", + DBType: "integer", + Default: "nextval('comms.text_job_id_seq'::regclass)", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + Type: column{ + Name: "type_", + DBType: "comms.textjobtype", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, + }, + Indexes: commsTextJobIndexes{ + TextJobPkey: index{ + Type: "btree", + Name: "text_job_pkey", + Columns: []indexColumn{ + { + Name: "id", + Desc: null.FromCond(false, true), + IsExpression: false, + }, + }, + Unique: true, + Comment: "", + NullsFirst: []bool{false}, + NullsDistinct: false, + Where: "", + Include: []string{}, + }, + }, + PrimaryKey: &constraint{ + Name: "text_job_pkey", + Columns: []string{"id"}, + Comment: "", + }, + ForeignKeys: commsTextJobForeignKeys{ + CommsTextJobTextJobDestinationFkey: foreignKey{ + constraint: constraint{ + Name: "comms.text_job.text_job_destination_fkey", + Columns: []string{"destination"}, + Comment: "", + }, + ForeignTable: "comms.phone", + ForeignColumns: []string{"e164"}, + }, + }, + + Comment: "Used to track text messages that should be sent later", +} + +type commsTextJobColumns struct { + Content column + Created column + Destination column + ID column + Type column +} + +func (c commsTextJobColumns) AsSlice() []column { + return []column{ + c.Content, c.Created, c.Destination, c.ID, c.Type, + } +} + +type commsTextJobIndexes struct { + TextJobPkey index +} + +func (i commsTextJobIndexes) AsSlice() []index { + return []index{ + i.TextJobPkey, + } +} + +type commsTextJobForeignKeys struct { + CommsTextJobTextJobDestinationFkey foreignKey +} + +func (f commsTextJobForeignKeys) AsSlice() []foreignKey { + return []foreignKey{ + f.CommsTextJobTextJobDestinationFkey, + } +} + +type commsTextJobUniques struct{} + +func (u commsTextJobUniques) AsSlice() []constraint { + return []constraint{} +} + +type commsTextJobChecks struct{} + +func (c commsTextJobChecks) AsSlice() []check { + return []check{} +} diff --git a/db/dbinfo/comms.text_log.bob.go b/db/dbinfo/comms.text_log.bob.go index 7447d67d..0e4a1329 100644 --- a/db/dbinfo/comms.text_log.bob.go +++ b/db/dbinfo/comms.text_log.bob.go @@ -51,6 +51,15 @@ var CommsTextLogs = Table[ Generated: false, AutoIncr: false, }, + IsWelcome: column{ + Name: "is_welcome", + DBType: "boolean", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, Origin: column{ Name: "origin", DBType: "comms.textorigin", @@ -115,7 +124,7 @@ var CommsTextLogs = Table[ }, }, - Comment: "", + Comment: "Used to track text messages that were sent.", } type commsTextLogColumns struct { @@ -123,13 +132,14 @@ type commsTextLogColumns struct { Created column Destination column ID column + IsWelcome column Origin column Source column } func (c commsTextLogColumns) AsSlice() []column { return []column{ - c.Content, c.Created, c.Destination, c.ID, c.Origin, c.Source, + c.Content, c.Created, c.Destination, c.ID, c.IsWelcome, c.Origin, c.Source, } } diff --git a/db/enums/enums.bob.go b/db/enums/enums.bob.go index 2cccb414..81f030ec 100644 --- a/db/enums/enums.bob.go +++ b/db/enums/enums.bob.go @@ -272,6 +272,76 @@ func (e *CommsMessagetypeemail) Scan(value any) error { return nil } +// Enum values for CommsTextjobtype +const ( + CommsTextjobtypeReportConfirmation CommsTextjobtype = "report-confirmation" +) + +func AllCommsTextjobtype() []CommsTextjobtype { + return []CommsTextjobtype{ + CommsTextjobtypeReportConfirmation, + } +} + +type CommsTextjobtype string + +func (e CommsTextjobtype) String() string { + return string(e) +} + +func (e CommsTextjobtype) Valid() bool { + switch e { + case CommsTextjobtypeReportConfirmation: + return true + default: + return false + } +} + +// useful when testing in other packages +func (e CommsTextjobtype) All() []CommsTextjobtype { + return AllCommsTextjobtype() +} + +func (e CommsTextjobtype) MarshalText() ([]byte, error) { + return []byte(e), nil +} + +func (e *CommsTextjobtype) UnmarshalText(text []byte) error { + return e.Scan(text) +} + +func (e CommsTextjobtype) MarshalBinary() ([]byte, error) { + return []byte(e), nil +} + +func (e *CommsTextjobtype) UnmarshalBinary(data []byte) error { + return e.Scan(data) +} + +func (e CommsTextjobtype) Value() (driver.Value, error) { + return string(e), nil +} + +func (e *CommsTextjobtype) Scan(value any) error { + switch x := value.(type) { + case string: + *e = CommsTextjobtype(x) + case []byte: + *e = CommsTextjobtype(x) + case nil: + return fmt.Errorf("cannot nil into CommsTextjobtype") + default: + return fmt.Errorf("cannot scan type %T: %v", value, value) + } + + if !e.Valid() { + return fmt.Errorf("invalid CommsTextjobtype value: %s", *e) + } + + return nil +} + // Enum values for CommsTextorigin const ( CommsTextoriginDistrict CommsTextorigin = "district" diff --git a/db/factory/bobfactory_context.bob.go b/db/factory/bobfactory_context.bob.go index f105c35c..4258ed4f 100644 --- a/db/factory/bobfactory_context.bob.go +++ b/db/factory/bobfactory_context.bob.go @@ -32,9 +32,14 @@ var ( // Relationship Contexts for comms.phone commsPhoneWithParentsCascadingCtx = newContextual[bool]("commsPhoneWithParentsCascading") + commsPhoneRelDestinationTextJobsCtx = newContextual[bool]("comms.phone.comms.text_job.comms.text_job.text_job_destination_fkey") commsPhoneRelDestinationTextLogsCtx = newContextual[bool]("comms.phone.comms.text_log.comms.text_log.text_log_destination_fkey") commsPhoneRelSourceTextLogsCtx = newContextual[bool]("comms.phone.comms.text_log.comms.text_log.text_log_source_fkey") + // Relationship Contexts for comms.text_job + commsTextJobWithParentsCascadingCtx = newContextual[bool]("commsTextJobWithParentsCascading") + commsTextJobRelDestinationPhoneCtx = newContextual[bool]("comms.phone.comms.text_job.comms.text_job.text_job_destination_fkey") + // Relationship Contexts for comms.text_log commsTextLogWithParentsCascadingCtx = newContextual[bool]("commsTextLogWithParentsCascading") commsTextLogRelDestinationPhoneCtx = newContextual[bool]("comms.phone.comms.text_log.comms.text_log.text_log_destination_fkey") diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index ab527cba..6e8d4391 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -25,6 +25,7 @@ type Factory struct { baseCommsEmailLogMods CommsEmailLogModSlice baseCommsEmailTemplateMods CommsEmailTemplateModSlice baseCommsPhoneMods CommsPhoneModSlice + baseCommsTextJobMods CommsTextJobModSlice baseCommsTextLogMods CommsTextLogModSlice baseFieldseekerContainerrelateMods FieldseekerContainerrelateModSlice baseFieldseekerFieldscoutinglogMods FieldseekerFieldscoutinglogModSlice @@ -295,6 +296,9 @@ func (f *Factory) FromExistingCommsPhone(m *models.CommsPhone) *CommsPhoneTempla o.IsSubscribed = func() bool { return m.IsSubscribed } ctx := context.Background() + if len(m.R.DestinationTextJobs) > 0 { + CommsPhoneMods.AddExistingDestinationTextJobs(m.R.DestinationTextJobs...).Apply(ctx, o) + } if len(m.R.DestinationTextLogs) > 0 { CommsPhoneMods.AddExistingDestinationTextLogs(m.R.DestinationTextLogs...).Apply(ctx, o) } @@ -305,6 +309,39 @@ func (f *Factory) FromExistingCommsPhone(m *models.CommsPhone) *CommsPhoneTempla return o } +func (f *Factory) NewCommsTextJob(mods ...CommsTextJobMod) *CommsTextJobTemplate { + return f.NewCommsTextJobWithContext(context.Background(), mods...) +} + +func (f *Factory) NewCommsTextJobWithContext(ctx context.Context, mods ...CommsTextJobMod) *CommsTextJobTemplate { + o := &CommsTextJobTemplate{f: f} + + if f != nil { + f.baseCommsTextJobMods.Apply(ctx, o) + } + + CommsTextJobModSlice(mods).Apply(ctx, o) + + return o +} + +func (f *Factory) FromExistingCommsTextJob(m *models.CommsTextJob) *CommsTextJobTemplate { + o := &CommsTextJobTemplate{f: f, alreadyPersisted: true} + + o.Content = func() string { return m.Content } + o.Created = func() time.Time { return m.Created } + o.Destination = func() string { return m.Destination } + o.ID = func() int32 { return m.ID } + o.Type = func() enums.CommsTextjobtype { return m.Type } + + ctx := context.Background() + if m.R.DestinationPhone != nil { + CommsTextJobMods.WithExistingDestinationPhone(m.R.DestinationPhone).Apply(ctx, o) + } + + return o +} + func (f *Factory) NewCommsTextLog(mods ...CommsTextLogMod) *CommsTextLogTemplate { return f.NewCommsTextLogWithContext(context.Background(), mods...) } @@ -328,6 +365,7 @@ func (f *Factory) FromExistingCommsTextLog(m *models.CommsTextLog) *CommsTextLog o.Created = func() time.Time { return m.Created } o.Destination = func() string { return m.Destination } o.ID = func() int32 { return m.ID } + o.IsWelcome = func() bool { return m.IsWelcome } o.Origin = func() enums.CommsTextorigin { return m.Origin } o.Source = func() string { return m.Source } @@ -3276,6 +3314,14 @@ func (f *Factory) AddBaseCommsPhoneMod(mods ...CommsPhoneMod) { f.baseCommsPhoneMods = append(f.baseCommsPhoneMods, mods...) } +func (f *Factory) ClearBaseCommsTextJobMods() { + f.baseCommsTextJobMods = nil +} + +func (f *Factory) AddBaseCommsTextJobMod(mods ...CommsTextJobMod) { + f.baseCommsTextJobMods = append(f.baseCommsTextJobMods, mods...) +} + func (f *Factory) ClearBaseCommsTextLogMods() { f.baseCommsTextLogMods = nil } diff --git a/db/factory/bobfactory_random.bob.go b/db/factory/bobfactory_random.bob.go index 23a5b12f..28ceac86 100644 --- a/db/factory/bobfactory_random.bob.go +++ b/db/factory/bobfactory_random.bob.go @@ -101,6 +101,16 @@ func random_enums_CommsMessagetypeemail(f *faker.Faker, limits ...string) enums. return all[f.IntBetween(0, len(all)-1)] } +func random_enums_CommsTextjobtype(f *faker.Faker, limits ...string) enums.CommsTextjobtype { + if f == nil { + f = &defaultFaker + } + + var e enums.CommsTextjobtype + all := e.All() + return all[f.IntBetween(0, len(all)-1)] +} + func random_enums_CommsTextorigin(f *faker.Faker, limits ...string) enums.CommsTextorigin { if f == nil { f = &defaultFaker diff --git a/db/factory/comms.phone.bob.go b/db/factory/comms.phone.bob.go index 415c6957..cefbb124 100644 --- a/db/factory/comms.phone.bob.go +++ b/db/factory/comms.phone.bob.go @@ -44,10 +44,15 @@ type CommsPhoneTemplate struct { } type commsPhoneR struct { + DestinationTextJobs []*commsPhoneRDestinationTextJobsR DestinationTextLogs []*commsPhoneRDestinationTextLogsR SourceTextLogs []*commsPhoneRSourceTextLogsR } +type commsPhoneRDestinationTextJobsR struct { + number int + o *CommsTextJobTemplate +} type commsPhoneRDestinationTextLogsR struct { number int o *CommsTextLogTemplate @@ -67,6 +72,19 @@ func (o *CommsPhoneTemplate) Apply(ctx context.Context, mods ...CommsPhoneMod) { // setModelRels creates and sets the relationships on *models.CommsPhone // according to the relationships in the template. Nothing is inserted into the db func (t CommsPhoneTemplate) setModelRels(o *models.CommsPhone) { + if t.r.DestinationTextJobs != nil { + rel := models.CommsTextJobSlice{} + for _, r := range t.r.DestinationTextJobs { + related := r.o.BuildMany(r.number) + for _, rel := range related { + rel.Destination = o.E164 // h2 + rel.R.DestinationPhone = o + } + rel = append(rel, related...) + } + o.R.DestinationTextJobs = rel + } + if t.r.DestinationTextLogs != nil { rel := models.CommsTextLogSlice{} for _, r := range t.r.DestinationTextLogs { @@ -171,6 +189,26 @@ func ensureCreatableCommsPhone(m *models.CommsPhoneSetter) { func (o *CommsPhoneTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.CommsPhone) error { var err error + isDestinationTextJobsDone, _ := commsPhoneRelDestinationTextJobsCtx.Value(ctx) + if !isDestinationTextJobsDone && o.r.DestinationTextJobs != nil { + ctx = commsPhoneRelDestinationTextJobsCtx.WithValue(ctx, true) + for _, r := range o.r.DestinationTextJobs { + if r.o.alreadyPersisted { + m.R.DestinationTextJobs = append(m.R.DestinationTextJobs, r.o.Build()) + } else { + rel0, err := r.o.CreateMany(ctx, exec, r.number) + if err != nil { + return err + } + + err = m.AttachDestinationTextJobs(ctx, exec, rel0...) + if err != nil { + return err + } + } + } + } + isDestinationTextLogsDone, _ := commsPhoneRelDestinationTextLogsCtx.Value(ctx) if !isDestinationTextLogsDone && o.r.DestinationTextLogs != nil { ctx = commsPhoneRelDestinationTextLogsCtx.WithValue(ctx, true) @@ -178,12 +216,12 @@ func (o *CommsPhoneTemplate) insertOptRels(ctx context.Context, exec bob.Executo if r.o.alreadyPersisted { m.R.DestinationTextLogs = append(m.R.DestinationTextLogs, r.o.Build()) } else { - rel0, err := r.o.CreateMany(ctx, exec, r.number) + rel1, err := r.o.CreateMany(ctx, exec, r.number) if err != nil { return err } - err = m.AttachDestinationTextLogs(ctx, exec, rel0...) + err = m.AttachDestinationTextLogs(ctx, exec, rel1...) if err != nil { return err } @@ -198,12 +236,12 @@ func (o *CommsPhoneTemplate) insertOptRels(ctx context.Context, exec bob.Executo if r.o.alreadyPersisted { m.R.SourceTextLogs = append(m.R.SourceTextLogs, r.o.Build()) } else { - rel1, err := r.o.CreateMany(ctx, exec, r.number) + rel2, err := r.o.CreateMany(ctx, exec, r.number) if err != nil { return err } - err = m.AttachSourceTextLogs(ctx, exec, rel1...) + err = m.AttachSourceTextLogs(ctx, exec, rel2...) if err != nil { return err } @@ -379,6 +417,54 @@ func (m commsPhoneMods) WithParentsCascading() CommsPhoneMod { }) } +func (m commsPhoneMods) WithDestinationTextJobs(number int, related *CommsTextJobTemplate) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + o.r.DestinationTextJobs = []*commsPhoneRDestinationTextJobsR{{ + number: number, + o: related, + }} + }) +} + +func (m commsPhoneMods) WithNewDestinationTextJobs(number int, mods ...CommsTextJobMod) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + related := o.f.NewCommsTextJobWithContext(ctx, mods...) + m.WithDestinationTextJobs(number, related).Apply(ctx, o) + }) +} + +func (m commsPhoneMods) AddDestinationTextJobs(number int, related *CommsTextJobTemplate) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + o.r.DestinationTextJobs = append(o.r.DestinationTextJobs, &commsPhoneRDestinationTextJobsR{ + number: number, + o: related, + }) + }) +} + +func (m commsPhoneMods) AddNewDestinationTextJobs(number int, mods ...CommsTextJobMod) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + related := o.f.NewCommsTextJobWithContext(ctx, mods...) + m.AddDestinationTextJobs(number, related).Apply(ctx, o) + }) +} + +func (m commsPhoneMods) AddExistingDestinationTextJobs(existingModels ...*models.CommsTextJob) CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + for _, em := range existingModels { + o.r.DestinationTextJobs = append(o.r.DestinationTextJobs, &commsPhoneRDestinationTextJobsR{ + o: o.f.FromExistingCommsTextJob(em), + }) + } + }) +} + +func (m commsPhoneMods) WithoutDestinationTextJobs() CommsPhoneMod { + return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { + o.r.DestinationTextJobs = nil + }) +} + func (m commsPhoneMods) WithDestinationTextLogs(number int, related *CommsTextLogTemplate) CommsPhoneMod { return CommsPhoneModFunc(func(ctx context.Context, o *CommsPhoneTemplate) { o.r.DestinationTextLogs = []*commsPhoneRDestinationTextLogsR{{ diff --git a/db/factory/comms.text_job.bob.go b/db/factory/comms.text_job.bob.go new file mode 100644 index 00000000..6d16388f --- /dev/null +++ b/db/factory/comms.text_job.bob.go @@ -0,0 +1,499 @@ +// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package factory + +import ( + "context" + "testing" + "time" + + enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" + models "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/aarondl/opt/omit" + "github.com/jaswdr/faker/v2" + "github.com/stephenafamo/bob" +) + +type CommsTextJobMod interface { + Apply(context.Context, *CommsTextJobTemplate) +} + +type CommsTextJobModFunc func(context.Context, *CommsTextJobTemplate) + +func (f CommsTextJobModFunc) Apply(ctx context.Context, n *CommsTextJobTemplate) { + f(ctx, n) +} + +type CommsTextJobModSlice []CommsTextJobMod + +func (mods CommsTextJobModSlice) Apply(ctx context.Context, n *CommsTextJobTemplate) { + for _, f := range mods { + f.Apply(ctx, n) + } +} + +// CommsTextJobTemplate is an object representing the database table. +// all columns are optional and should be set by mods +type CommsTextJobTemplate struct { + Content func() string + Created func() time.Time + Destination func() string + ID func() int32 + Type func() enums.CommsTextjobtype + + r commsTextJobR + f *Factory + + alreadyPersisted bool +} + +type commsTextJobR struct { + DestinationPhone *commsTextJobRDestinationPhoneR +} + +type commsTextJobRDestinationPhoneR struct { + o *CommsPhoneTemplate +} + +// Apply mods to the CommsTextJobTemplate +func (o *CommsTextJobTemplate) Apply(ctx context.Context, mods ...CommsTextJobMod) { + for _, mod := range mods { + mod.Apply(ctx, o) + } +} + +// setModelRels creates and sets the relationships on *models.CommsTextJob +// according to the relationships in the template. Nothing is inserted into the db +func (t CommsTextJobTemplate) setModelRels(o *models.CommsTextJob) { + if t.r.DestinationPhone != nil { + rel := t.r.DestinationPhone.o.Build() + rel.R.DestinationTextJobs = append(rel.R.DestinationTextJobs, o) + o.Destination = rel.E164 // h2 + o.R.DestinationPhone = rel + } +} + +// BuildSetter returns an *models.CommsTextJobSetter +// this does nothing with the relationship templates +func (o CommsTextJobTemplate) BuildSetter() *models.CommsTextJobSetter { + m := &models.CommsTextJobSetter{} + + if o.Content != nil { + val := o.Content() + m.Content = omit.From(val) + } + if o.Created != nil { + val := o.Created() + m.Created = omit.From(val) + } + if o.Destination != nil { + val := o.Destination() + m.Destination = omit.From(val) + } + if o.ID != nil { + val := o.ID() + m.ID = omit.From(val) + } + if o.Type != nil { + val := o.Type() + m.Type = omit.From(val) + } + + return m +} + +// BuildManySetter returns an []*models.CommsTextJobSetter +// this does nothing with the relationship templates +func (o CommsTextJobTemplate) BuildManySetter(number int) []*models.CommsTextJobSetter { + m := make([]*models.CommsTextJobSetter, number) + + for i := range m { + m[i] = o.BuildSetter() + } + + return m +} + +// Build returns an *models.CommsTextJob +// Related objects are also created and placed in the .R field +// NOTE: Objects are not inserted into the database. Use CommsTextJobTemplate.Create +func (o CommsTextJobTemplate) Build() *models.CommsTextJob { + m := &models.CommsTextJob{} + + if o.Content != nil { + m.Content = o.Content() + } + if o.Created != nil { + m.Created = o.Created() + } + if o.Destination != nil { + m.Destination = o.Destination() + } + if o.ID != nil { + m.ID = o.ID() + } + if o.Type != nil { + m.Type = o.Type() + } + + o.setModelRels(m) + + return m +} + +// BuildMany returns an models.CommsTextJobSlice +// Related objects are also created and placed in the .R field +// NOTE: Objects are not inserted into the database. Use CommsTextJobTemplate.CreateMany +func (o CommsTextJobTemplate) BuildMany(number int) models.CommsTextJobSlice { + m := make(models.CommsTextJobSlice, number) + + for i := range m { + m[i] = o.Build() + } + + return m +} + +func ensureCreatableCommsTextJob(m *models.CommsTextJobSetter) { + if !(m.Content.IsValue()) { + val := random_string(nil) + m.Content = omit.From(val) + } + if !(m.Created.IsValue()) { + val := random_time_Time(nil) + m.Created = omit.From(val) + } + if !(m.Destination.IsValue()) { + val := random_string(nil) + m.Destination = omit.From(val) + } + if !(m.Type.IsValue()) { + val := random_enums_CommsTextjobtype(nil) + m.Type = omit.From(val) + } +} + +// insertOptRels creates and inserts any optional the relationships on *models.CommsTextJob +// according to the relationships in the template. +// any required relationship should have already exist on the model +func (o *CommsTextJobTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.CommsTextJob) error { + var err error + + return err +} + +// Create builds a commsTextJob and inserts it into the database +// Relations objects are also inserted and placed in the .R field +func (o *CommsTextJobTemplate) Create(ctx context.Context, exec bob.Executor) (*models.CommsTextJob, error) { + var err error + opt := o.BuildSetter() + ensureCreatableCommsTextJob(opt) + + if o.r.DestinationPhone == nil { + CommsTextJobMods.WithNewDestinationPhone().Apply(ctx, o) + } + + var rel0 *models.CommsPhone + + if o.r.DestinationPhone.o.alreadyPersisted { + rel0 = o.r.DestinationPhone.o.Build() + } else { + rel0, err = o.r.DestinationPhone.o.Create(ctx, exec) + if err != nil { + return nil, err + } + } + + opt.Destination = omit.From(rel0.E164) + + m, err := models.CommsTextJobs.Insert(opt).One(ctx, exec) + if err != nil { + return nil, err + } + + m.R.DestinationPhone = rel0 + + if err := o.insertOptRels(ctx, exec, m); err != nil { + return nil, err + } + return m, err +} + +// MustCreate builds a commsTextJob and inserts it into the database +// Relations objects are also inserted and placed in the .R field +// panics if an error occurs +func (o *CommsTextJobTemplate) MustCreate(ctx context.Context, exec bob.Executor) *models.CommsTextJob { + m, err := o.Create(ctx, exec) + if err != nil { + panic(err) + } + return m +} + +// CreateOrFail builds a commsTextJob and inserts it into the database +// Relations objects are also inserted and placed in the .R field +// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs +func (o *CommsTextJobTemplate) CreateOrFail(ctx context.Context, tb testing.TB, exec bob.Executor) *models.CommsTextJob { + tb.Helper() + m, err := o.Create(ctx, exec) + if err != nil { + tb.Fatal(err) + return nil + } + return m +} + +// CreateMany builds multiple commsTextJobs and inserts them into the database +// Relations objects are also inserted and placed in the .R field +func (o CommsTextJobTemplate) CreateMany(ctx context.Context, exec bob.Executor, number int) (models.CommsTextJobSlice, error) { + var err error + m := make(models.CommsTextJobSlice, number) + + for i := range m { + m[i], err = o.Create(ctx, exec) + if err != nil { + return nil, err + } + } + + return m, nil +} + +// MustCreateMany builds multiple commsTextJobs and inserts them into the database +// Relations objects are also inserted and placed in the .R field +// panics if an error occurs +func (o CommsTextJobTemplate) MustCreateMany(ctx context.Context, exec bob.Executor, number int) models.CommsTextJobSlice { + m, err := o.CreateMany(ctx, exec, number) + if err != nil { + panic(err) + } + return m +} + +// CreateManyOrFail builds multiple commsTextJobs and inserts them into the database +// Relations objects are also inserted and placed in the .R field +// It calls `tb.Fatal(err)` on the test/benchmark if an error occurs +func (o CommsTextJobTemplate) CreateManyOrFail(ctx context.Context, tb testing.TB, exec bob.Executor, number int) models.CommsTextJobSlice { + tb.Helper() + m, err := o.CreateMany(ctx, exec, number) + if err != nil { + tb.Fatal(err) + return nil + } + return m +} + +// CommsTextJob has methods that act as mods for the CommsTextJobTemplate +var CommsTextJobMods commsTextJobMods + +type commsTextJobMods struct{} + +func (m commsTextJobMods) RandomizeAllColumns(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModSlice{ + CommsTextJobMods.RandomContent(f), + CommsTextJobMods.RandomCreated(f), + CommsTextJobMods.RandomDestination(f), + CommsTextJobMods.RandomID(f), + CommsTextJobMods.RandomType(f), + } +} + +// Set the model columns to this value +func (m commsTextJobMods) Content(val string) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Content = func() string { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) ContentFunc(f func() string) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Content = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetContent() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Content = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomContent(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Content = func() string { + return random_string(f) + } + }) +} + +// Set the model columns to this value +func (m commsTextJobMods) Created(val time.Time) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Created = func() time.Time { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) CreatedFunc(f func() time.Time) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Created = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetCreated() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Created = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomCreated(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Created = func() time.Time { + return random_time_Time(f) + } + }) +} + +// Set the model columns to this value +func (m commsTextJobMods) Destination(val string) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Destination = func() string { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) DestinationFunc(f func() string) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Destination = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetDestination() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Destination = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomDestination(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Destination = func() string { + return random_string(f) + } + }) +} + +// Set the model columns to this value +func (m commsTextJobMods) ID(val int32) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.ID = func() int32 { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) IDFunc(f func() int32) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.ID = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetID() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.ID = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomID(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.ID = func() int32 { + return random_int32(f) + } + }) +} + +// Set the model columns to this value +func (m commsTextJobMods) Type(val enums.CommsTextjobtype) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Type = func() enums.CommsTextjobtype { return val } + }) +} + +// Set the Column from the function +func (m commsTextJobMods) TypeFunc(f func() enums.CommsTextjobtype) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Type = f + }) +} + +// Clear any values for the column +func (m commsTextJobMods) UnsetType() CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Type = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextJobMods) RandomType(f *faker.Faker) CommsTextJobMod { + return CommsTextJobModFunc(func(_ context.Context, o *CommsTextJobTemplate) { + o.Type = func() enums.CommsTextjobtype { + return random_enums_CommsTextjobtype(f) + } + }) +} + +func (m commsTextJobMods) WithParentsCascading() CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + if isDone, _ := commsTextJobWithParentsCascadingCtx.Value(ctx); isDone { + return + } + ctx = commsTextJobWithParentsCascadingCtx.WithValue(ctx, true) + { + + related := o.f.NewCommsPhoneWithContext(ctx, CommsPhoneMods.WithParentsCascading()) + m.WithDestinationPhone(related).Apply(ctx, o) + } + }) +} + +func (m commsTextJobMods) WithDestinationPhone(rel *CommsPhoneTemplate) CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + o.r.DestinationPhone = &commsTextJobRDestinationPhoneR{ + o: rel, + } + }) +} + +func (m commsTextJobMods) WithNewDestinationPhone(mods ...CommsPhoneMod) CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + related := o.f.NewCommsPhoneWithContext(ctx, mods...) + + m.WithDestinationPhone(related).Apply(ctx, o) + }) +} + +func (m commsTextJobMods) WithExistingDestinationPhone(em *models.CommsPhone) CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + o.r.DestinationPhone = &commsTextJobRDestinationPhoneR{ + o: o.f.FromExistingCommsPhone(em), + } + }) +} + +func (m commsTextJobMods) WithoutDestinationPhone() CommsTextJobMod { + return CommsTextJobModFunc(func(ctx context.Context, o *CommsTextJobTemplate) { + o.r.DestinationPhone = nil + }) +} diff --git a/db/factory/comms.text_log.bob.go b/db/factory/comms.text_log.bob.go index 6b1cd6e0..d2e88519 100644 --- a/db/factory/comms.text_log.bob.go +++ b/db/factory/comms.text_log.bob.go @@ -40,6 +40,7 @@ type CommsTextLogTemplate struct { Created func() time.Time Destination func() string ID func() int32 + IsWelcome func() bool Origin func() enums.CommsTextorigin Source func() string @@ -107,6 +108,10 @@ func (o CommsTextLogTemplate) BuildSetter() *models.CommsTextLogSetter { val := o.ID() m.ID = omit.From(val) } + if o.IsWelcome != nil { + val := o.IsWelcome() + m.IsWelcome = omit.From(val) + } if o.Origin != nil { val := o.Origin() m.Origin = omit.From(val) @@ -149,6 +154,9 @@ func (o CommsTextLogTemplate) Build() *models.CommsTextLog { if o.ID != nil { m.ID = o.ID() } + if o.IsWelcome != nil { + m.IsWelcome = o.IsWelcome() + } if o.Origin != nil { m.Origin = o.Origin() } @@ -187,6 +195,10 @@ func ensureCreatableCommsTextLog(m *models.CommsTextLogSetter) { val := random_string(nil) m.Destination = omit.From(val) } + if !(m.IsWelcome.IsValue()) { + val := random_bool(nil) + m.IsWelcome = omit.From(val) + } if !(m.Origin.IsValue()) { val := random_enums_CommsTextorigin(nil) m.Origin = omit.From(val) @@ -336,6 +348,7 @@ func (m commsTextLogMods) RandomizeAllColumns(f *faker.Faker) CommsTextLogMod { CommsTextLogMods.RandomCreated(f), CommsTextLogMods.RandomDestination(f), CommsTextLogMods.RandomID(f), + CommsTextLogMods.RandomIsWelcome(f), CommsTextLogMods.RandomOrigin(f), CommsTextLogMods.RandomSource(f), } @@ -465,6 +478,37 @@ func (m commsTextLogMods) RandomID(f *faker.Faker) CommsTextLogMod { }) } +// Set the model columns to this value +func (m commsTextLogMods) IsWelcome(val bool) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsWelcome = func() bool { return val } + }) +} + +// Set the Column from the function +func (m commsTextLogMods) IsWelcomeFunc(f func() bool) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsWelcome = f + }) +} + +// Clear any values for the column +func (m commsTextLogMods) UnsetIsWelcome() CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsWelcome = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextLogMods) RandomIsWelcome(f *faker.Faker) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsWelcome = func() bool { + return random_bool(f) + } + }) +} + // Set the model columns to this value func (m commsTextLogMods) Origin(val enums.CommsTextorigin) CommsTextLogMod { return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { diff --git a/db/migrations/00041_text_log_overhaul.sql b/db/migrations/00041_text_log_overhaul.sql index 4e33d846..2a954d31 100644 --- a/db/migrations/00041_text_log_overhaul.sql +++ b/db/migrations/00041_text_log_overhaul.sql @@ -6,18 +6,34 @@ CREATE TYPE comms.TextOrigin AS ENUM ( 'llm', 'website-action' ); +CREATE TYPE comms.TextJobType AS ENUM ( + 'report-confirmation' +); +CREATE TABLE comms.text_job ( + content TEXT NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + destination TEXT NOT NULL REFERENCES comms.phone(e164), + id SERIAL NOT NULL, + type_ comms.TextJobType NOT NULL, + PRIMARY KEY(id) +); +COMMENT ON TABLE comms.text_job IS 'Used to track text messages that should be sent later'; CREATE TABLE comms.text_log ( content TEXT NOT NULL, created TIMESTAMP WITHOUT TIME ZONE NOT NULL, destination TEXT NOT NULL REFERENCES comms.phone(e164), id SERIAL, + is_welcome BOOLEAN NOT NULL, origin comms.TextOrigin NOT NULL, source TEXT NOT NULL REFERENCES comms.phone(e164), PRIMARY KEY(id) ); +COMMENT ON TABLE comms.text_log IS 'Used to track text messages that were sent.'; -- +goose Down DROP TABLE comms.text_log; +DROP TABLE comms.text_job; +DROP TYPE comms.TextJobType; DROP TYPE comms.TextOrigin; CREATE TYPE comms.MessageTypeText AS ENUM ( 'initial-contact', diff --git a/db/models/bob_joins.bob.go b/db/models/bob_joins.bob.go index e6ca18af..a69510f4 100644 --- a/db/models/bob_joins.bob.go +++ b/db/models/bob_joins.bob.go @@ -38,6 +38,7 @@ type joins[Q dialect.Joinable] struct { CommsEmailLogs joinSet[commsEmailLogJoins[Q]] CommsEmailTemplates joinSet[commsEmailTemplateJoins[Q]] CommsPhones joinSet[commsPhoneJoins[Q]] + CommsTextJobs joinSet[commsTextJobJoins[Q]] CommsTextLogs joinSet[commsTextLogJoins[Q]] FieldseekerContainerrelates joinSet[fieldseekerContainerrelateJoins[Q]] FieldseekerFieldscoutinglogs joinSet[fieldseekerFieldscoutinglogJoins[Q]] @@ -104,6 +105,7 @@ func getJoins[Q dialect.Joinable]() joins[Q] { CommsEmailLogs: buildJoinSet[commsEmailLogJoins[Q]](CommsEmailLogs.Columns, buildCommsEmailLogJoins), CommsEmailTemplates: buildJoinSet[commsEmailTemplateJoins[Q]](CommsEmailTemplates.Columns, buildCommsEmailTemplateJoins), CommsPhones: buildJoinSet[commsPhoneJoins[Q]](CommsPhones.Columns, buildCommsPhoneJoins), + CommsTextJobs: buildJoinSet[commsTextJobJoins[Q]](CommsTextJobs.Columns, buildCommsTextJobJoins), CommsTextLogs: buildJoinSet[commsTextLogJoins[Q]](CommsTextLogs.Columns, buildCommsTextLogJoins), FieldseekerContainerrelates: buildJoinSet[fieldseekerContainerrelateJoins[Q]](FieldseekerContainerrelates.Columns, buildFieldseekerContainerrelateJoins), FieldseekerFieldscoutinglogs: buildJoinSet[fieldseekerFieldscoutinglogJoins[Q]](FieldseekerFieldscoutinglogs.Columns, buildFieldseekerFieldscoutinglogJoins), diff --git a/db/models/bob_loaders.bob.go b/db/models/bob_loaders.bob.go index aff45d78..3a003482 100644 --- a/db/models/bob_loaders.bob.go +++ b/db/models/bob_loaders.bob.go @@ -23,6 +23,7 @@ type preloaders struct { CommsEmailLog commsEmailLogPreloader CommsEmailTemplate commsEmailTemplatePreloader CommsPhone commsPhonePreloader + CommsTextJob commsTextJobPreloader CommsTextLog commsTextLogPreloader FieldseekerContainerrelate fieldseekerContainerrelatePreloader FieldseekerFieldscoutinglog fieldseekerFieldscoutinglogPreloader @@ -81,6 +82,7 @@ func getPreloaders() preloaders { CommsEmailLog: buildCommsEmailLogPreloader(), CommsEmailTemplate: buildCommsEmailTemplatePreloader(), CommsPhone: buildCommsPhonePreloader(), + CommsTextJob: buildCommsTextJobPreloader(), CommsTextLog: buildCommsTextLogPreloader(), FieldseekerContainerrelate: buildFieldseekerContainerrelatePreloader(), FieldseekerFieldscoutinglog: buildFieldseekerFieldscoutinglogPreloader(), @@ -145,6 +147,7 @@ type thenLoaders[Q orm.Loadable] struct { CommsEmailLog commsEmailLogThenLoader[Q] CommsEmailTemplate commsEmailTemplateThenLoader[Q] CommsPhone commsPhoneThenLoader[Q] + CommsTextJob commsTextJobThenLoader[Q] CommsTextLog commsTextLogThenLoader[Q] FieldseekerContainerrelate fieldseekerContainerrelateThenLoader[Q] FieldseekerFieldscoutinglog fieldseekerFieldscoutinglogThenLoader[Q] @@ -203,6 +206,7 @@ func getThenLoaders[Q orm.Loadable]() thenLoaders[Q] { CommsEmailLog: buildCommsEmailLogThenLoader[Q](), CommsEmailTemplate: buildCommsEmailTemplateThenLoader[Q](), CommsPhone: buildCommsPhoneThenLoader[Q](), + CommsTextJob: buildCommsTextJobThenLoader[Q](), CommsTextLog: buildCommsTextLogThenLoader[Q](), FieldseekerContainerrelate: buildFieldseekerContainerrelateThenLoader[Q](), FieldseekerFieldscoutinglog: buildFieldseekerFieldscoutinglogThenLoader[Q](), diff --git a/db/models/bob_where.bob.go b/db/models/bob_where.bob.go index fad47793..d25228a0 100644 --- a/db/models/bob_where.bob.go +++ b/db/models/bob_where.bob.go @@ -23,6 +23,7 @@ func Where[Q psql.Filterable]() struct { CommsEmailLogs commsEmailLogWhere[Q] CommsEmailTemplates commsEmailTemplateWhere[Q] CommsPhones commsPhoneWhere[Q] + CommsTextJobs commsTextJobWhere[Q] CommsTextLogs commsTextLogWhere[Q] FieldseekerContainerrelates fieldseekerContainerrelateWhere[Q] FieldseekerFieldscoutinglogs fieldseekerFieldscoutinglogWhere[Q] @@ -87,6 +88,7 @@ func Where[Q psql.Filterable]() struct { CommsEmailLogs commsEmailLogWhere[Q] CommsEmailTemplates commsEmailTemplateWhere[Q] CommsPhones commsPhoneWhere[Q] + CommsTextJobs commsTextJobWhere[Q] CommsTextLogs commsTextLogWhere[Q] FieldseekerContainerrelates fieldseekerContainerrelateWhere[Q] FieldseekerFieldscoutinglogs fieldseekerFieldscoutinglogWhere[Q] @@ -150,6 +152,7 @@ func Where[Q psql.Filterable]() struct { CommsEmailLogs: buildCommsEmailLogWhere[Q](CommsEmailLogs.Columns), CommsEmailTemplates: buildCommsEmailTemplateWhere[Q](CommsEmailTemplates.Columns), CommsPhones: buildCommsPhoneWhere[Q](CommsPhones.Columns), + CommsTextJobs: buildCommsTextJobWhere[Q](CommsTextJobs.Columns), CommsTextLogs: buildCommsTextLogWhere[Q](CommsTextLogs.Columns), FieldseekerContainerrelates: buildFieldseekerContainerrelateWhere[Q](FieldseekerContainerrelates.Columns), FieldseekerFieldscoutinglogs: buildFieldseekerFieldscoutinglogWhere[Q](FieldseekerFieldscoutinglogs.Columns), diff --git a/db/models/comms.phone.bob.go b/db/models/comms.phone.bob.go index ffc12632..6af2d6e7 100644 --- a/db/models/comms.phone.bob.go +++ b/db/models/comms.phone.bob.go @@ -43,6 +43,7 @@ type CommsPhonesQuery = *psql.ViewQuery[*CommsPhone, CommsPhoneSlice] // commsPhoneR is where relationships are stored. type commsPhoneR struct { + DestinationTextJobs CommsTextJobSlice // comms.text_job.text_job_destination_fkey DestinationTextLogs CommsTextLogSlice // comms.text_log.text_log_destination_fkey SourceTextLogs CommsTextLogSlice // comms.text_log.text_log_source_fkey } @@ -371,6 +372,30 @@ func (o CommsPhoneSlice) ReloadAll(ctx context.Context, exec bob.Executor) error return nil } +// DestinationTextJobs starts a query for related objects on comms.text_job +func (o *CommsPhone) DestinationTextJobs(mods ...bob.Mod[*dialect.SelectQuery]) CommsTextJobsQuery { + return CommsTextJobs.Query(append(mods, + sm.Where(CommsTextJobs.Columns.Destination.EQ(psql.Arg(o.E164))), + )...) +} + +func (os CommsPhoneSlice) DestinationTextJobs(mods ...bob.Mod[*dialect.SelectQuery]) CommsTextJobsQuery { + pkE164 := make(pgtypes.Array[string], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkE164 = append(pkE164, o.E164) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkE164), "text[]")), + )) + + return CommsTextJobs.Query(append(mods, + sm.Where(psql.Group(CommsTextJobs.Columns.Destination).OP("IN", PKArgExpr)), + )...) +} + // DestinationTextLogs starts a query for related objects on comms.text_log func (o *CommsPhone) DestinationTextLogs(mods ...bob.Mod[*dialect.SelectQuery]) CommsTextLogsQuery { return CommsTextLogs.Query(append(mods, @@ -419,6 +444,74 @@ func (os CommsPhoneSlice) SourceTextLogs(mods ...bob.Mod[*dialect.SelectQuery]) )...) } +func insertCommsPhoneDestinationTextJobs0(ctx context.Context, exec bob.Executor, commsTextJobs1 []*CommsTextJobSetter, commsPhone0 *CommsPhone) (CommsTextJobSlice, error) { + for i := range commsTextJobs1 { + commsTextJobs1[i].Destination = omit.From(commsPhone0.E164) + } + + ret, err := CommsTextJobs.Insert(bob.ToMods(commsTextJobs1...)).All(ctx, exec) + if err != nil { + return ret, fmt.Errorf("insertCommsPhoneDestinationTextJobs0: %w", err) + } + + return ret, nil +} + +func attachCommsPhoneDestinationTextJobs0(ctx context.Context, exec bob.Executor, count int, commsTextJobs1 CommsTextJobSlice, commsPhone0 *CommsPhone) (CommsTextJobSlice, error) { + setter := &CommsTextJobSetter{ + Destination: omit.From(commsPhone0.E164), + } + + err := commsTextJobs1.UpdateAll(ctx, exec, *setter) + if err != nil { + return nil, fmt.Errorf("attachCommsPhoneDestinationTextJobs0: %w", err) + } + + return commsTextJobs1, nil +} + +func (commsPhone0 *CommsPhone) InsertDestinationTextJobs(ctx context.Context, exec bob.Executor, related ...*CommsTextJobSetter) error { + if len(related) == 0 { + return nil + } + + var err error + + commsTextJobs1, err := insertCommsPhoneDestinationTextJobs0(ctx, exec, related, commsPhone0) + if err != nil { + return err + } + + commsPhone0.R.DestinationTextJobs = append(commsPhone0.R.DestinationTextJobs, commsTextJobs1...) + + for _, rel := range commsTextJobs1 { + rel.R.DestinationPhone = commsPhone0 + } + return nil +} + +func (commsPhone0 *CommsPhone) AttachDestinationTextJobs(ctx context.Context, exec bob.Executor, related ...*CommsTextJob) error { + if len(related) == 0 { + return nil + } + + var err error + commsTextJobs1 := CommsTextJobSlice(related) + + _, err = attachCommsPhoneDestinationTextJobs0(ctx, exec, len(related), commsTextJobs1, commsPhone0) + if err != nil { + return err + } + + commsPhone0.R.DestinationTextJobs = append(commsPhone0.R.DestinationTextJobs, commsTextJobs1...) + + for _, rel := range related { + rel.R.DestinationPhone = commsPhone0 + } + + return nil +} + func insertCommsPhoneDestinationTextLogs0(ctx context.Context, exec bob.Executor, commsTextLogs1 []*CommsTextLogSetter, commsPhone0 *CommsPhone) (CommsTextLogSlice, error) { for i := range commsTextLogs1 { commsTextLogs1[i].Destination = omit.From(commsPhone0.E164) @@ -577,6 +670,20 @@ func (o *CommsPhone) Preload(name string, retrieved any) error { } switch name { + case "DestinationTextJobs": + rels, ok := retrieved.(CommsTextJobSlice) + if !ok { + return fmt.Errorf("commsPhone cannot load %T as %q", retrieved, name) + } + + o.R.DestinationTextJobs = rels + + for _, rel := range rels { + if rel != nil { + rel.R.DestinationPhone = o + } + } + return nil case "DestinationTextLogs": rels, ok := retrieved.(CommsTextLogSlice) if !ok { @@ -617,11 +724,15 @@ func buildCommsPhonePreloader() commsPhonePreloader { } type commsPhoneThenLoader[Q orm.Loadable] struct { + DestinationTextJobs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] DestinationTextLogs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] SourceTextLogs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] } func buildCommsPhoneThenLoader[Q orm.Loadable]() commsPhoneThenLoader[Q] { + type DestinationTextJobsLoadInterface interface { + LoadDestinationTextJobs(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } type DestinationTextLogsLoadInterface interface { LoadDestinationTextLogs(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } @@ -630,6 +741,12 @@ func buildCommsPhoneThenLoader[Q orm.Loadable]() commsPhoneThenLoader[Q] { } return commsPhoneThenLoader[Q]{ + DestinationTextJobs: thenLoadBuilder[Q]( + "DestinationTextJobs", + func(ctx context.Context, exec bob.Executor, retrieved DestinationTextJobsLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadDestinationTextJobs(ctx, exec, mods...) + }, + ), DestinationTextLogs: thenLoadBuilder[Q]( "DestinationTextLogs", func(ctx context.Context, exec bob.Executor, retrieved DestinationTextLogsLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { @@ -645,6 +762,67 @@ func buildCommsPhoneThenLoader[Q orm.Loadable]() commsPhoneThenLoader[Q] { } } +// LoadDestinationTextJobs loads the commsPhone's DestinationTextJobs into the .R struct +func (o *CommsPhone) LoadDestinationTextJobs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.DestinationTextJobs = nil + + related, err := o.DestinationTextJobs(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, rel := range related { + rel.R.DestinationPhone = o + } + + o.R.DestinationTextJobs = related + return nil +} + +// LoadDestinationTextJobs loads the commsPhone's DestinationTextJobs into the .R struct +func (os CommsPhoneSlice) LoadDestinationTextJobs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + commsTextJobs, err := os.DestinationTextJobs(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + o.R.DestinationTextJobs = nil + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range commsTextJobs { + + if !(o.E164 == rel.Destination) { + continue + } + + rel.R.DestinationPhone = o + + o.R.DestinationTextJobs = append(o.R.DestinationTextJobs, rel) + } + } + + return nil +} + // LoadDestinationTextLogs loads the commsPhone's DestinationTextLogs into the .R struct func (o *CommsPhone) LoadDestinationTextLogs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { if o == nil { @@ -769,6 +947,7 @@ func (os CommsPhoneSlice) LoadSourceTextLogs(ctx context.Context, exec bob.Execu // commsPhoneC is where relationship counts are stored. type commsPhoneC struct { + DestinationTextJobs *int64 DestinationTextLogs *int64 SourceTextLogs *int64 } @@ -780,6 +959,8 @@ func (o *CommsPhone) PreloadCount(name string, count int64) error { } switch name { + case "DestinationTextJobs": + o.C.DestinationTextJobs = &count case "DestinationTextLogs": o.C.DestinationTextLogs = &count case "SourceTextLogs": @@ -789,12 +970,30 @@ func (o *CommsPhone) PreloadCount(name string, count int64) error { } type commsPhoneCountPreloader struct { + DestinationTextJobs func(...bob.Mod[*dialect.SelectQuery]) psql.Preloader DestinationTextLogs func(...bob.Mod[*dialect.SelectQuery]) psql.Preloader SourceTextLogs func(...bob.Mod[*dialect.SelectQuery]) psql.Preloader } func buildCommsPhoneCountPreloader() commsPhoneCountPreloader { return commsPhoneCountPreloader{ + DestinationTextJobs: func(mods ...bob.Mod[*dialect.SelectQuery]) psql.Preloader { + return countPreloader[*CommsPhone]("DestinationTextJobs", func(parent string) bob.Expression { + // Build a correlated subquery: (SELECT COUNT(*) FROM related WHERE fk = parent.pk) + if parent == "" { + parent = CommsPhones.Alias() + } + + subqueryMods := []bob.Mod[*dialect.SelectQuery]{ + sm.Columns(psql.Raw("count(*)")), + + sm.From(CommsTextJobs.Name()), + sm.Where(psql.Quote(CommsTextJobs.Alias(), "destination").EQ(psql.Quote(parent, "e164"))), + } + subqueryMods = append(subqueryMods, mods...) + return psql.Group(psql.Select(subqueryMods...).Expression) + }) + }, DestinationTextLogs: func(mods ...bob.Mod[*dialect.SelectQuery]) psql.Preloader { return countPreloader[*CommsPhone]("DestinationTextLogs", func(parent string) bob.Expression { // Build a correlated subquery: (SELECT COUNT(*) FROM related WHERE fk = parent.pk) @@ -833,11 +1032,15 @@ func buildCommsPhoneCountPreloader() commsPhoneCountPreloader { } type commsPhoneCountThenLoader[Q orm.Loadable] struct { + DestinationTextJobs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] DestinationTextLogs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] SourceTextLogs func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] } func buildCommsPhoneCountThenLoader[Q orm.Loadable]() commsPhoneCountThenLoader[Q] { + type DestinationTextJobsCountInterface interface { + LoadCountDestinationTextJobs(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } type DestinationTextLogsCountInterface interface { LoadCountDestinationTextLogs(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error } @@ -846,6 +1049,12 @@ func buildCommsPhoneCountThenLoader[Q orm.Loadable]() commsPhoneCountThenLoader[ } return commsPhoneCountThenLoader[Q]{ + DestinationTextJobs: countThenLoadBuilder[Q]( + "DestinationTextJobs", + func(ctx context.Context, exec bob.Executor, retrieved DestinationTextJobsCountInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadCountDestinationTextJobs(ctx, exec, mods...) + }, + ), DestinationTextLogs: countThenLoadBuilder[Q]( "DestinationTextLogs", func(ctx context.Context, exec bob.Executor, retrieved DestinationTextLogsCountInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { @@ -861,6 +1070,36 @@ func buildCommsPhoneCountThenLoader[Q orm.Loadable]() commsPhoneCountThenLoader[ } } +// LoadCountDestinationTextJobs loads the count of DestinationTextJobs into the C struct +func (o *CommsPhone) LoadCountDestinationTextJobs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + count, err := o.DestinationTextJobs(mods...).Count(ctx, exec) + if err != nil { + return err + } + + o.C.DestinationTextJobs = &count + return nil +} + +// LoadCountDestinationTextJobs loads the count of DestinationTextJobs for a slice +func (os CommsPhoneSlice) LoadCountDestinationTextJobs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + for _, o := range os { + if err := o.LoadCountDestinationTextJobs(ctx, exec, mods...); err != nil { + return err + } + } + + return nil +} + // LoadCountDestinationTextLogs loads the count of DestinationTextLogs into the C struct func (o *CommsPhone) LoadCountDestinationTextLogs(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { if o == nil { @@ -923,6 +1162,7 @@ func (os CommsPhoneSlice) LoadCountSourceTextLogs(ctx context.Context, exec bob. type commsPhoneJoins[Q dialect.Joinable] struct { typ string + DestinationTextJobs modAs[Q, commsTextJobColumns] DestinationTextLogs modAs[Q, commsTextLogColumns] SourceTextLogs modAs[Q, commsTextLogColumns] } @@ -934,6 +1174,20 @@ func (j commsPhoneJoins[Q]) aliasedAs(alias string) commsPhoneJoins[Q] { func buildCommsPhoneJoins[Q dialect.Joinable](cols commsPhoneColumns, typ string) commsPhoneJoins[Q] { return commsPhoneJoins[Q]{ typ: typ, + DestinationTextJobs: modAs[Q, commsTextJobColumns]{ + c: CommsTextJobs.Columns, + f: func(to commsTextJobColumns) bob.Mod[Q] { + mods := make(mods.QueryMods[Q], 0, 1) + + { + mods = append(mods, dialect.Join[Q](typ, CommsTextJobs.Name().As(to.Alias())).On( + to.Destination.EQ(cols.E164), + )) + } + + return mods + }, + }, DestinationTextLogs: modAs[Q, commsTextLogColumns]{ c: CommsTextLogs.Columns, f: func(to commsTextLogColumns) bob.Mod[Q] { diff --git a/db/models/comms.text_job.bob.go b/db/models/comms.text_job.bob.go new file mode 100644 index 00000000..6a8ce5b5 --- /dev/null +++ b/db/models/comms.text_job.bob.go @@ -0,0 +1,679 @@ +// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "context" + "fmt" + "io" + "time" + + enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/aarondl/opt/omit" + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/psql" + "github.com/stephenafamo/bob/dialect/psql/dialect" + "github.com/stephenafamo/bob/dialect/psql/dm" + "github.com/stephenafamo/bob/dialect/psql/sm" + "github.com/stephenafamo/bob/dialect/psql/um" + "github.com/stephenafamo/bob/expr" + "github.com/stephenafamo/bob/mods" + "github.com/stephenafamo/bob/orm" + "github.com/stephenafamo/bob/types/pgtypes" +) + +// CommsTextJob is an object representing the database table. +type CommsTextJob struct { + Content string `db:"content" ` + Created time.Time `db:"created" ` + Destination string `db:"destination" ` + ID int32 `db:"id,pk" ` + Type enums.CommsTextjobtype `db:"type_" ` + + R commsTextJobR `db:"-" ` +} + +// CommsTextJobSlice is an alias for a slice of pointers to CommsTextJob. +// This should almost always be used instead of []*CommsTextJob. +type CommsTextJobSlice []*CommsTextJob + +// CommsTextJobs contains methods to work with the text_job table +var CommsTextJobs = psql.NewTablex[*CommsTextJob, CommsTextJobSlice, *CommsTextJobSetter]("comms", "text_job", buildCommsTextJobColumns("comms.text_job")) + +// CommsTextJobsQuery is a query on the text_job table +type CommsTextJobsQuery = *psql.ViewQuery[*CommsTextJob, CommsTextJobSlice] + +// commsTextJobR is where relationships are stored. +type commsTextJobR struct { + DestinationPhone *CommsPhone // comms.text_job.text_job_destination_fkey +} + +func buildCommsTextJobColumns(alias string) commsTextJobColumns { + return commsTextJobColumns{ + ColumnsExpr: expr.NewColumnsExpr( + "content", "created", "destination", "id", "type_", + ).WithParent("comms.text_job"), + tableAlias: alias, + Content: psql.Quote(alias, "content"), + Created: psql.Quote(alias, "created"), + Destination: psql.Quote(alias, "destination"), + ID: psql.Quote(alias, "id"), + Type: psql.Quote(alias, "type_"), + } +} + +type commsTextJobColumns struct { + expr.ColumnsExpr + tableAlias string + Content psql.Expression + Created psql.Expression + Destination psql.Expression + ID psql.Expression + Type psql.Expression +} + +func (c commsTextJobColumns) Alias() string { + return c.tableAlias +} + +func (commsTextJobColumns) AliasedAs(alias string) commsTextJobColumns { + return buildCommsTextJobColumns(alias) +} + +// CommsTextJobSetter is used for insert/upsert/update operations +// All values are optional, and do not have to be set +// Generated columns are not included +type CommsTextJobSetter struct { + Content omit.Val[string] `db:"content" ` + Created omit.Val[time.Time] `db:"created" ` + Destination omit.Val[string] `db:"destination" ` + ID omit.Val[int32] `db:"id,pk" ` + Type omit.Val[enums.CommsTextjobtype] `db:"type_" ` +} + +func (s CommsTextJobSetter) SetColumns() []string { + vals := make([]string, 0, 5) + if s.Content.IsValue() { + vals = append(vals, "content") + } + if s.Created.IsValue() { + vals = append(vals, "created") + } + if s.Destination.IsValue() { + vals = append(vals, "destination") + } + if s.ID.IsValue() { + vals = append(vals, "id") + } + if s.Type.IsValue() { + vals = append(vals, "type_") + } + return vals +} + +func (s CommsTextJobSetter) Overwrite(t *CommsTextJob) { + if s.Content.IsValue() { + t.Content = s.Content.MustGet() + } + if s.Created.IsValue() { + t.Created = s.Created.MustGet() + } + if s.Destination.IsValue() { + t.Destination = s.Destination.MustGet() + } + if s.ID.IsValue() { + t.ID = s.ID.MustGet() + } + if s.Type.IsValue() { + t.Type = s.Type.MustGet() + } +} + +func (s *CommsTextJobSetter) Apply(q *dialect.InsertQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return CommsTextJobs.BeforeInsertHooks.RunHooks(ctx, exec, s) + }) + + q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + vals := make([]bob.Expression, 5) + if s.Content.IsValue() { + vals[0] = psql.Arg(s.Content.MustGet()) + } else { + vals[0] = psql.Raw("DEFAULT") + } + + if s.Created.IsValue() { + vals[1] = psql.Arg(s.Created.MustGet()) + } else { + vals[1] = psql.Raw("DEFAULT") + } + + if s.Destination.IsValue() { + vals[2] = psql.Arg(s.Destination.MustGet()) + } else { + vals[2] = psql.Raw("DEFAULT") + } + + if s.ID.IsValue() { + vals[3] = psql.Arg(s.ID.MustGet()) + } else { + vals[3] = psql.Raw("DEFAULT") + } + + if s.Type.IsValue() { + vals[4] = psql.Arg(s.Type.MustGet()) + } else { + vals[4] = psql.Raw("DEFAULT") + } + + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") + })) +} + +func (s CommsTextJobSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { + return um.Set(s.Expressions()...) +} + +func (s CommsTextJobSetter) Expressions(prefix ...string) []bob.Expression { + exprs := make([]bob.Expression, 0, 5) + + if s.Content.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "content")...), + psql.Arg(s.Content), + }}) + } + + if s.Created.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "created")...), + psql.Arg(s.Created), + }}) + } + + if s.Destination.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "destination")...), + psql.Arg(s.Destination), + }}) + } + + if s.ID.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "id")...), + psql.Arg(s.ID), + }}) + } + + if s.Type.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "type_")...), + psql.Arg(s.Type), + }}) + } + + return exprs +} + +// FindCommsTextJob retrieves a single record by primary key +// If cols is empty Find will return all columns. +func FindCommsTextJob(ctx context.Context, exec bob.Executor, IDPK int32, cols ...string) (*CommsTextJob, error) { + if len(cols) == 0 { + return CommsTextJobs.Query( + sm.Where(CommsTextJobs.Columns.ID.EQ(psql.Arg(IDPK))), + ).One(ctx, exec) + } + + return CommsTextJobs.Query( + sm.Where(CommsTextJobs.Columns.ID.EQ(psql.Arg(IDPK))), + sm.Columns(CommsTextJobs.Columns.Only(cols...)), + ).One(ctx, exec) +} + +// CommsTextJobExists checks the presence of a single record by primary key +func CommsTextJobExists(ctx context.Context, exec bob.Executor, IDPK int32) (bool, error) { + return CommsTextJobs.Query( + sm.Where(CommsTextJobs.Columns.ID.EQ(psql.Arg(IDPK))), + ).Exists(ctx, exec) +} + +// AfterQueryHook is called after CommsTextJob is retrieved from the database +func (o *CommsTextJob) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = CommsTextJobs.AfterSelectHooks.RunHooks(ctx, exec, CommsTextJobSlice{o}) + case bob.QueryTypeInsert: + ctx, err = CommsTextJobs.AfterInsertHooks.RunHooks(ctx, exec, CommsTextJobSlice{o}) + case bob.QueryTypeUpdate: + ctx, err = CommsTextJobs.AfterUpdateHooks.RunHooks(ctx, exec, CommsTextJobSlice{o}) + case bob.QueryTypeDelete: + ctx, err = CommsTextJobs.AfterDeleteHooks.RunHooks(ctx, exec, CommsTextJobSlice{o}) + } + + return err +} + +// primaryKeyVals returns the primary key values of the CommsTextJob +func (o *CommsTextJob) primaryKeyVals() bob.Expression { + return psql.Arg(o.ID) +} + +func (o *CommsTextJob) pkEQ() dialect.Expression { + return psql.Quote("comms.text_job", "id").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + return o.primaryKeyVals().WriteSQL(ctx, w, d, start) + })) +} + +// Update uses an executor to update the CommsTextJob +func (o *CommsTextJob) Update(ctx context.Context, exec bob.Executor, s *CommsTextJobSetter) error { + v, err := CommsTextJobs.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec) + if err != nil { + return err + } + + o.R = v.R + *o = *v + + return nil +} + +// Delete deletes a single CommsTextJob record with an executor +func (o *CommsTextJob) Delete(ctx context.Context, exec bob.Executor) error { + _, err := CommsTextJobs.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec) + return err +} + +// Reload refreshes the CommsTextJob using the executor +func (o *CommsTextJob) Reload(ctx context.Context, exec bob.Executor) error { + o2, err := CommsTextJobs.Query( + sm.Where(CommsTextJobs.Columns.ID.EQ(psql.Arg(o.ID))), + ).One(ctx, exec) + if err != nil { + return err + } + o2.R = o.R + *o = *o2 + + return nil +} + +// AfterQueryHook is called after CommsTextJobSlice is retrieved from the database +func (o CommsTextJobSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error { + var err error + + switch queryType { + case bob.QueryTypeSelect: + ctx, err = CommsTextJobs.AfterSelectHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeInsert: + ctx, err = CommsTextJobs.AfterInsertHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeUpdate: + ctx, err = CommsTextJobs.AfterUpdateHooks.RunHooks(ctx, exec, o) + case bob.QueryTypeDelete: + ctx, err = CommsTextJobs.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err +} + +func (o CommsTextJobSlice) pkIN() dialect.Expression { + if len(o) == 0 { + return psql.Raw("NULL") + } + + return psql.Quote("comms.text_job", "id").In(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + pkPairs := make([]bob.Expression, len(o)) + for i, row := range o { + pkPairs[i] = row.primaryKeyVals() + } + return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "") + })) +} + +// copyMatchingRows finds models in the given slice that have the same primary key +// then it first copies the existing relationships from the old model to the new model +// and then replaces the old model in the slice with the new model +func (o CommsTextJobSlice) copyMatchingRows(from ...*CommsTextJob) { + for i, old := range o { + for _, new := range from { + if new.ID != old.ID { + continue + } + new.R = old.R + o[i] = new + break + } + } +} + +// UpdateMod modifies an update query with "WHERE primary_key IN (o...)" +func (o CommsTextJobSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] { + return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return CommsTextJobs.BeforeUpdateHooks.RunHooks(ctx, exec, o) + }) + + q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error { + var err error + switch retrieved := retrieved.(type) { + case *CommsTextJob: + o.copyMatchingRows(retrieved) + case []*CommsTextJob: + o.copyMatchingRows(retrieved...) + case CommsTextJobSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a CommsTextJob or a slice of CommsTextJob + // then run the AfterUpdateHooks on the slice + _, err = CommsTextJobs.AfterUpdateHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)" +func (o CommsTextJobSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] { + return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) { + q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) { + return CommsTextJobs.BeforeDeleteHooks.RunHooks(ctx, exec, o) + }) + + q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error { + var err error + switch retrieved := retrieved.(type) { + case *CommsTextJob: + o.copyMatchingRows(retrieved) + case []*CommsTextJob: + o.copyMatchingRows(retrieved...) + case CommsTextJobSlice: + o.copyMatchingRows(retrieved...) + default: + // If the retrieved value is not a CommsTextJob or a slice of CommsTextJob + // then run the AfterDeleteHooks on the slice + _, err = CommsTextJobs.AfterDeleteHooks.RunHooks(ctx, exec, o) + } + + return err + })) + + q.AppendWhere(o.pkIN()) + }) +} + +func (o CommsTextJobSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals CommsTextJobSetter) error { + if len(o) == 0 { + return nil + } + + _, err := CommsTextJobs.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec) + return err +} + +func (o CommsTextJobSlice) DeleteAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + _, err := CommsTextJobs.Delete(o.DeleteMod()).Exec(ctx, exec) + return err +} + +func (o CommsTextJobSlice) ReloadAll(ctx context.Context, exec bob.Executor) error { + if len(o) == 0 { + return nil + } + + o2, err := CommsTextJobs.Query(sm.Where(o.pkIN())).All(ctx, exec) + if err != nil { + return err + } + + o.copyMatchingRows(o2...) + + return nil +} + +// DestinationPhone starts a query for related objects on comms.phone +func (o *CommsTextJob) DestinationPhone(mods ...bob.Mod[*dialect.SelectQuery]) CommsPhonesQuery { + return CommsPhones.Query(append(mods, + sm.Where(CommsPhones.Columns.E164.EQ(psql.Arg(o.Destination))), + )...) +} + +func (os CommsTextJobSlice) DestinationPhone(mods ...bob.Mod[*dialect.SelectQuery]) CommsPhonesQuery { + pkDestination := make(pgtypes.Array[string], 0, len(os)) + for _, o := range os { + if o == nil { + continue + } + pkDestination = append(pkDestination, o.Destination) + } + PKArgExpr := psql.Select(sm.Columns( + psql.F("unnest", psql.Cast(psql.Arg(pkDestination), "text[]")), + )) + + return CommsPhones.Query(append(mods, + sm.Where(psql.Group(CommsPhones.Columns.E164).OP("IN", PKArgExpr)), + )...) +} + +func attachCommsTextJobDestinationPhone0(ctx context.Context, exec bob.Executor, count int, commsTextJob0 *CommsTextJob, commsPhone1 *CommsPhone) (*CommsTextJob, error) { + setter := &CommsTextJobSetter{ + Destination: omit.From(commsPhone1.E164), + } + + err := commsTextJob0.Update(ctx, exec, setter) + if err != nil { + return nil, fmt.Errorf("attachCommsTextJobDestinationPhone0: %w", err) + } + + return commsTextJob0, nil +} + +func (commsTextJob0 *CommsTextJob) InsertDestinationPhone(ctx context.Context, exec bob.Executor, related *CommsPhoneSetter) error { + var err error + + commsPhone1, err := CommsPhones.Insert(related).One(ctx, exec) + if err != nil { + return fmt.Errorf("inserting related objects: %w", err) + } + + _, err = attachCommsTextJobDestinationPhone0(ctx, exec, 1, commsTextJob0, commsPhone1) + if err != nil { + return err + } + + commsTextJob0.R.DestinationPhone = commsPhone1 + + commsPhone1.R.DestinationTextJobs = append(commsPhone1.R.DestinationTextJobs, commsTextJob0) + + return nil +} + +func (commsTextJob0 *CommsTextJob) AttachDestinationPhone(ctx context.Context, exec bob.Executor, commsPhone1 *CommsPhone) error { + var err error + + _, err = attachCommsTextJobDestinationPhone0(ctx, exec, 1, commsTextJob0, commsPhone1) + if err != nil { + return err + } + + commsTextJob0.R.DestinationPhone = commsPhone1 + + commsPhone1.R.DestinationTextJobs = append(commsPhone1.R.DestinationTextJobs, commsTextJob0) + + return nil +} + +type commsTextJobWhere[Q psql.Filterable] struct { + Content psql.WhereMod[Q, string] + Created psql.WhereMod[Q, time.Time] + Destination psql.WhereMod[Q, string] + ID psql.WhereMod[Q, int32] + Type psql.WhereMod[Q, enums.CommsTextjobtype] +} + +func (commsTextJobWhere[Q]) AliasedAs(alias string) commsTextJobWhere[Q] { + return buildCommsTextJobWhere[Q](buildCommsTextJobColumns(alias)) +} + +func buildCommsTextJobWhere[Q psql.Filterable](cols commsTextJobColumns) commsTextJobWhere[Q] { + return commsTextJobWhere[Q]{ + Content: psql.Where[Q, string](cols.Content), + Created: psql.Where[Q, time.Time](cols.Created), + Destination: psql.Where[Q, string](cols.Destination), + ID: psql.Where[Q, int32](cols.ID), + Type: psql.Where[Q, enums.CommsTextjobtype](cols.Type), + } +} + +func (o *CommsTextJob) Preload(name string, retrieved any) error { + if o == nil { + return nil + } + + switch name { + case "DestinationPhone": + rel, ok := retrieved.(*CommsPhone) + if !ok { + return fmt.Errorf("commsTextJob cannot load %T as %q", retrieved, name) + } + + o.R.DestinationPhone = rel + + if rel != nil { + rel.R.DestinationTextJobs = CommsTextJobSlice{o} + } + return nil + default: + return fmt.Errorf("commsTextJob has no relationship %q", name) + } +} + +type commsTextJobPreloader struct { + DestinationPhone func(...psql.PreloadOption) psql.Preloader +} + +func buildCommsTextJobPreloader() commsTextJobPreloader { + return commsTextJobPreloader{ + DestinationPhone: func(opts ...psql.PreloadOption) psql.Preloader { + return psql.Preload[*CommsPhone, CommsPhoneSlice](psql.PreloadRel{ + Name: "DestinationPhone", + Sides: []psql.PreloadSide{ + { + From: CommsTextJobs, + To: CommsPhones, + FromColumns: []string{"destination"}, + ToColumns: []string{"e164"}, + }, + }, + }, CommsPhones.Columns.Names(), opts...) + }, + } +} + +type commsTextJobThenLoader[Q orm.Loadable] struct { + DestinationPhone func(...bob.Mod[*dialect.SelectQuery]) orm.Loader[Q] +} + +func buildCommsTextJobThenLoader[Q orm.Loadable]() commsTextJobThenLoader[Q] { + type DestinationPhoneLoadInterface interface { + LoadDestinationPhone(context.Context, bob.Executor, ...bob.Mod[*dialect.SelectQuery]) error + } + + return commsTextJobThenLoader[Q]{ + DestinationPhone: thenLoadBuilder[Q]( + "DestinationPhone", + func(ctx context.Context, exec bob.Executor, retrieved DestinationPhoneLoadInterface, mods ...bob.Mod[*dialect.SelectQuery]) error { + return retrieved.LoadDestinationPhone(ctx, exec, mods...) + }, + ), + } +} + +// LoadDestinationPhone loads the commsTextJob's DestinationPhone into the .R struct +func (o *CommsTextJob) LoadDestinationPhone(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if o == nil { + return nil + } + + // Reset the relationship + o.R.DestinationPhone = nil + + related, err := o.DestinationPhone(mods...).One(ctx, exec) + if err != nil { + return err + } + + related.R.DestinationTextJobs = CommsTextJobSlice{o} + + o.R.DestinationPhone = related + return nil +} + +// LoadDestinationPhone loads the commsTextJob's DestinationPhone into the .R struct +func (os CommsTextJobSlice) LoadDestinationPhone(ctx context.Context, exec bob.Executor, mods ...bob.Mod[*dialect.SelectQuery]) error { + if len(os) == 0 { + return nil + } + + commsPhones, err := os.DestinationPhone(mods...).All(ctx, exec) + if err != nil { + return err + } + + for _, o := range os { + if o == nil { + continue + } + + for _, rel := range commsPhones { + + if !(o.Destination == rel.E164) { + continue + } + + rel.R.DestinationTextJobs = append(rel.R.DestinationTextJobs, o) + + o.R.DestinationPhone = rel + break + } + } + + return nil +} + +type commsTextJobJoins[Q dialect.Joinable] struct { + typ string + DestinationPhone modAs[Q, commsPhoneColumns] +} + +func (j commsTextJobJoins[Q]) aliasedAs(alias string) commsTextJobJoins[Q] { + return buildCommsTextJobJoins[Q](buildCommsTextJobColumns(alias), j.typ) +} + +func buildCommsTextJobJoins[Q dialect.Joinable](cols commsTextJobColumns, typ string) commsTextJobJoins[Q] { + return commsTextJobJoins[Q]{ + typ: typ, + DestinationPhone: modAs[Q, commsPhoneColumns]{ + c: CommsPhones.Columns, + f: func(to commsPhoneColumns) bob.Mod[Q] { + mods := make(mods.QueryMods[Q], 0, 1) + + { + mods = append(mods, dialect.Join[Q](typ, CommsPhones.Name().As(to.Alias())).On( + to.E164.EQ(cols.Destination), + )) + } + + return mods + }, + }, + } +} diff --git a/db/models/comms.text_log.bob.go b/db/models/comms.text_log.bob.go index fc1054d3..445ac37f 100644 --- a/db/models/comms.text_log.bob.go +++ b/db/models/comms.text_log.bob.go @@ -29,6 +29,7 @@ type CommsTextLog struct { Created time.Time `db:"created" ` Destination string `db:"destination" ` ID int32 `db:"id,pk" ` + IsWelcome bool `db:"is_welcome" ` Origin enums.CommsTextorigin `db:"origin" ` Source string `db:"source" ` @@ -54,13 +55,14 @@ type commsTextLogR struct { func buildCommsTextLogColumns(alias string) commsTextLogColumns { return commsTextLogColumns{ ColumnsExpr: expr.NewColumnsExpr( - "content", "created", "destination", "id", "origin", "source", + "content", "created", "destination", "id", "is_welcome", "origin", "source", ).WithParent("comms.text_log"), tableAlias: alias, Content: psql.Quote(alias, "content"), Created: psql.Quote(alias, "created"), Destination: psql.Quote(alias, "destination"), ID: psql.Quote(alias, "id"), + IsWelcome: psql.Quote(alias, "is_welcome"), Origin: psql.Quote(alias, "origin"), Source: psql.Quote(alias, "source"), } @@ -73,6 +75,7 @@ type commsTextLogColumns struct { Created psql.Expression Destination psql.Expression ID psql.Expression + IsWelcome psql.Expression Origin psql.Expression Source psql.Expression } @@ -93,12 +96,13 @@ type CommsTextLogSetter struct { Created omit.Val[time.Time] `db:"created" ` Destination omit.Val[string] `db:"destination" ` ID omit.Val[int32] `db:"id,pk" ` + IsWelcome omit.Val[bool] `db:"is_welcome" ` Origin omit.Val[enums.CommsTextorigin] `db:"origin" ` Source omit.Val[string] `db:"source" ` } func (s CommsTextLogSetter) SetColumns() []string { - vals := make([]string, 0, 6) + vals := make([]string, 0, 7) if s.Content.IsValue() { vals = append(vals, "content") } @@ -111,6 +115,9 @@ func (s CommsTextLogSetter) SetColumns() []string { if s.ID.IsValue() { vals = append(vals, "id") } + if s.IsWelcome.IsValue() { + vals = append(vals, "is_welcome") + } if s.Origin.IsValue() { vals = append(vals, "origin") } @@ -133,6 +140,9 @@ func (s CommsTextLogSetter) Overwrite(t *CommsTextLog) { if s.ID.IsValue() { t.ID = s.ID.MustGet() } + if s.IsWelcome.IsValue() { + t.IsWelcome = s.IsWelcome.MustGet() + } if s.Origin.IsValue() { t.Origin = s.Origin.MustGet() } @@ -147,7 +157,7 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { }) q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { - vals := make([]bob.Expression, 6) + vals := make([]bob.Expression, 7) if s.Content.IsValue() { vals[0] = psql.Arg(s.Content.MustGet()) } else { @@ -172,18 +182,24 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { vals[3] = psql.Raw("DEFAULT") } - if s.Origin.IsValue() { - vals[4] = psql.Arg(s.Origin.MustGet()) + if s.IsWelcome.IsValue() { + vals[4] = psql.Arg(s.IsWelcome.MustGet()) } else { vals[4] = psql.Raw("DEFAULT") } - if s.Source.IsValue() { - vals[5] = psql.Arg(s.Source.MustGet()) + if s.Origin.IsValue() { + vals[5] = psql.Arg(s.Origin.MustGet()) } else { vals[5] = psql.Raw("DEFAULT") } + if s.Source.IsValue() { + vals[6] = psql.Arg(s.Source.MustGet()) + } else { + vals[6] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -193,7 +209,7 @@ func (s CommsTextLogSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 6) + exprs := make([]bob.Expression, 0, 7) if s.Content.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -223,6 +239,13 @@ func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if s.IsWelcome.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "is_welcome")...), + psql.Arg(s.IsWelcome), + }}) + } + if s.Origin.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "origin")...), @@ -612,6 +635,7 @@ type commsTextLogWhere[Q psql.Filterable] struct { Created psql.WhereMod[Q, time.Time] Destination psql.WhereMod[Q, string] ID psql.WhereMod[Q, int32] + IsWelcome psql.WhereMod[Q, bool] Origin psql.WhereMod[Q, enums.CommsTextorigin] Source psql.WhereMod[Q, string] } @@ -626,6 +650,7 @@ func buildCommsTextLogWhere[Q psql.Filterable](cols commsTextLogColumns) commsTe Created: psql.Where[Q, time.Time](cols.Created), Destination: psql.Where[Q, string](cols.Destination), ID: psql.Where[Q, int32](cols.ID), + IsWelcome: psql.Where[Q, bool](cols.IsWelcome), Origin: psql.Where[Q, enums.CommsTextorigin](cols.Origin), Source: psql.Where[Q, string](cols.Source), } diff --git a/main.go b/main.go index ed8956b6..ac76e6af 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/background" "github.com/Gleipnir-Technology/nidus-sync/comms/email" + "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/public-report" @@ -46,6 +47,11 @@ func main() { os.Exit(3) } + err = text.StoreSources() + if err != nil { + log.Error().Err(err).Msg("Failed to store text source phone numbers") + os.Exit(4) + } router_logger := log.With().Logger() r := chi.NewRouter() From 940f3901be9fdf65f57e15c60790ad3821298a20 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 16:11:00 +0000 Subject: [PATCH 0137/1513] Redirect root mock to additional mocks --- public-report/template/mock/root.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public-report/template/mock/root.html b/public-report/template/mock/root.html index 02d7ae16..fb2ac179 100644 --- a/public-report/template/mock/root.html +++ b/public-report/template/mock/root.html @@ -52,7 +52,7 @@

Report Mosquito Nuisance

Report areas with high adult mosquito activity causing discomfort or concern.

- Report Problem + Report Problem
@@ -64,7 +64,7 @@

Report Standing Water

Report any water that has been sitting for several days, where mosquitoes can live.

- Report Source + Report Source
@@ -76,7 +76,7 @@

Follow-up or Check Status

Check on a previous request or view current mosquito activity in your area.

- Get Status + Get Status
From c276cbac0b50e399055114801d4458157712537c Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 18:42:30 +0000 Subject: [PATCH 0138/1513] Add command to hash password directly Useful for resetting passwords manually. --- auth/auth.go | 6 +++--- cmd/passwordgen/main.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 cmd/passwordgen/main.go diff --git a/auth/auth.go b/auth/auth.go index 688ec2c6..f2f8d0b5 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -131,7 +131,7 @@ func SignoutUser(r *http.Request, user *models.User) { } func SignupUser(ctx context.Context, username string, name string, password string) (*models.User, error) { - passwordHash, err := hashPassword(password) + passwordHash, err := HashPassword(password) if err != nil { return nil, fmt.Errorf("Cannot signup user, failed to create hashed password: %w", err) } @@ -182,7 +182,7 @@ func findUser(ctx context.Context, user_id int) (*models.User, error) { return user, err } -func hashPassword(password string) (string, error) { +func HashPassword(password string) (string, error) { bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) return string(bytes), err } @@ -205,7 +205,7 @@ func validatePassword(password, hash string) bool { } func validateUser(ctx context.Context, username string, password string) (*models.User, error) { - passwordHash, err := hashPassword(password) + passwordHash, err := HashPassword(password) if err != nil { return nil, fmt.Errorf("Failed to hash password: %w", err) } diff --git a/cmd/passwordgen/main.go b/cmd/passwordgen/main.go new file mode 100644 index 00000000..e0b3aad4 --- /dev/null +++ b/cmd/passwordgen/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "log" + "os" + + "github.com/Gleipnir-Technology/nidus-sync/auth" +) + +func main() { + var password string + scanValue("Please enter your password : ", &password) + + hash, err := auth.HashPassword(password) + if err != nil { + fmt.Printf("Failed to hash password: %v\n", err) + os.Exit(1) + } + + fmt.Println("Password:", password) + fmt.Println("Hash: ", hash) + +} + +func scanValue(message string, result *string) { + fmt.Printf(message) + scanner := bufio.NewScanner(os.Stdin) + if ok := scanner.Scan(); !ok { + log.Fatal(errors.New("Failed to scan input")) + } + *result = scanner.Text() +} From 6070d50a5895dfcf00f00f22c33593f857b125b4 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 20:29:04 +0000 Subject: [PATCH 0139/1513] Begin process of getting text responses from an LLM. --- api/twilio.go | 9 +- comms/text/db.go | 3 +- comms/text/initial.go | 8 +- comms/text/llm.go | 9 ++ comms/text/report-subscription.go | 2 +- comms/text/text.go | 4 +- config/config.go | 14 ++- db/sql/texts_by_senders.bob.go | 135 +++++++++++++++++++++++ db/sql/texts_by_senders.bob.sql | 20 ++++ db/sql/texts_by_senders.sql | 17 +++ go.mod | 26 +++-- go.sum | 38 +++++++ llm/client.go | 22 ++++ llm/log.go | 42 +++++++ llm/openai.go | 175 ++++++++++++++++++++++++++++++ main.go | 7 ++ platform/text.go | 113 +++++++++++++++++++ tools/texts_by_senders.sql | 17 +++ 18 files changed, 639 insertions(+), 22 deletions(-) create mode 100644 comms/text/llm.go create mode 100644 db/sql/texts_by_senders.bob.go create mode 100644 db/sql/texts_by_senders.bob.sql create mode 100644 db/sql/texts_by_senders.sql create mode 100644 llm/client.go create mode 100644 llm/log.go create mode 100644 llm/openai.go create mode 100644 platform/text.go create mode 100644 tools/texts_by_senders.sql diff --git a/api/twilio.go b/api/twilio.go index 7323eb69..9a252e80 100644 --- a/api/twilio.go +++ b/api/twilio.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" + "github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/rs/zerolog/log" "github.com/twilio/twilio-go/twiml" ) @@ -39,11 +40,9 @@ func twilioTextPost(w http.ResponseWriter, r *http.Request) { to_zip := r.PostFormValue("ToZip") to_country := r.PostFormValue("ToCountry") log.Info().Str("message_sid", message_sid).Str("account_sid", account_sid).Str("messaging_service_sid", messaging_service_sid).Str("from", from).Str("to_", to_).Str("body", body).Str("num_media", num_media).Str("num_segments", num_segments).Str("media_content_type0", media_content_type0).Str("media_url0", media_url0).Str("from_city", from_city).Str("from_state", from_state).Str("from_zip", from_zip).Str("from_country", from_country).Str("to_city", to_city).Str("to_state", to_state).Str("to_zip", to_zip).Str("to_country", to_country).Msg("got text") - twiml, _ := twiml.Messages([]twiml.Element{ - &twiml.MessagingMessage{ - Body: "Hey there.", - }, - }) + + twiml, _ := twiml.Messages([]twiml.Element{}) + go platform.HandleTextMessage(from, to_, body) w.Header().Set("Content-Type", "text/xml") fmt.Fprintf(w, "%s", twiml) } diff --git a/comms/text/db.go b/comms/text/db.go index 1795a061..9801e1ca 100644 --- a/comms/text/db.go +++ b/comms/text/db.go @@ -68,12 +68,13 @@ func ensureInDB(ctx context.Context, destination string) (err error) { return nil } -func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin) (err error) { +func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool) (err error) { _, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ //ID: Content: omit.From(content), Created: omit.From(time.Now()), Destination: omit.From(destination), + IsWelcome: omit.From(is_welcome), Origin: omit.From(origin), Source: omit.From(source), }).One(ctx, db.PGInstance.BobDB) diff --git a/comms/text/initial.go b/comms/text/initial.go index d2bf35a5..0c25a62c 100644 --- a/comms/text/initial.go +++ b/comms/text/initial.go @@ -23,9 +23,15 @@ func ensureInitialText(ctx context.Context, src string, dst string) error { return nil } content := "Welcome to Report Mosquitoes Online. We received your request and want to confirm text updates. Reply YES to continue. Reply STOP at any time to unsubscribe" - err = sendText(ctx, src, dst, content, origin) + err = sendText(ctx, src, dst, content, origin, true) if err != nil { return fmt.Errorf("Failed to send initial confirmation: %w", err) } return nil } + +func SendInitialReprompt(ctx context.Context, src string, dst string) error { + content := "I have to start with either 'YES' or 'STOP' first, Which do you want?" + err := sendText(ctx, src, dst, content, enums.CommsTextoriginLLM, false) + return err +} diff --git a/comms/text/llm.go b/comms/text/llm.go new file mode 100644 index 00000000..efd664fa --- /dev/null +++ b/comms/text/llm.go @@ -0,0 +1,9 @@ +package text + +import ( + "github.com/rs/zerolog/log" +) + +func SendTextFromLLM(content string) { + log.Info().Str("content", content).Msg("Pretend I sent a message") +} diff --git a/comms/text/report-subscription.go b/comms/text/report-subscription.go index 092b9ee2..c43f6b89 100644 --- a/comms/text/report-subscription.go +++ b/comms/text/report-subscription.go @@ -53,7 +53,7 @@ func sendReportSubscription(ctx context.Context, job Job) error { return fmt.Errorf("Failed to check if subscribed: %w", err) } if !sub { - err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction) + err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction, false) if err != nil { return fmt.Errorf("Failed to send report subscription confirmation: %w", err) } diff --git a/comms/text/text.go b/comms/text/text.go index 415a301d..8384405f 100644 --- a/comms/text/text.go +++ b/comms/text/text.go @@ -19,12 +19,12 @@ func ParsePhoneNumber(input string) (*E164, error) { return phonenumbers.Parse(input, "US") } -func sendText(ctx context.Context, source string, destination string, message string, origin enums.CommsTextorigin) error { +func sendText(ctx context.Context, source string, destination string, message string, origin enums.CommsTextorigin, is_welcome bool) error { err := ensureInDB(ctx, destination) if err != nil { return fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err) } - err = insertTextLog(ctx, message, destination, source, origin) + err = insertTextLog(ctx, message, destination, source, origin, is_welcome) if err != nil { return fmt.Errorf("Failed to insert text message in the DB: %w", err) } diff --git a/config/config.go b/config/config.go index 09b356f2..9b7461ab 100644 --- a/config/config.go +++ b/config/config.go @@ -27,9 +27,11 @@ var ( MapboxToken string PGDSN string PhoneNumberReport phonenumbers.PhoneNumber + PhoneNumberReportStr string TwilioAuthToken string TwilioAccountSID string TwilioMessagingServiceSID string + TwilioRCSSenderRMO string ) func IsProductionEnvironment() bool { @@ -127,13 +129,13 @@ func Parse() (err error) { if PGDSN == "" { return fmt.Errorf("You must specify a non-empty POSTGRES_DSN") } - rmo_phone_number := os.Getenv("RMO_PHONE_NUMBER") - if rmo_phone_number == "" { + PhoneNumberReportStr = os.Getenv("RMO_PHONE_NUMBER") + if PhoneNumberReportStr == "" { return fmt.Errorf("You must specify a non-empty RMO_PHONE_NUMBER") } - p, err := phonenumbers.Parse(rmo_phone_number, "US") + p, err := phonenumbers.Parse(PhoneNumberReportStr, "US") if err != nil { - return fmt.Errorf("Failed to parse '%s' as a valid phone number: %w", rmo_phone_number, err) + return fmt.Errorf("Failed to parse '%s' as a valid phone number: %w", PhoneNumberReportStr, err) } PhoneNumberReport = *p @@ -149,6 +151,10 @@ func Parse() (err error) { if TwilioMessagingServiceSID == "" { return fmt.Errorf("You must specify a non-empty TWILIO_MESSAGING_SERVICE_SID") } + TwilioRCSSenderRMO = os.Getenv("TWILIO_RCS_SENDER_RMO") + if TwilioRCSSenderRMO == "" { + return fmt.Errorf("You must specify a non-empty TWILIO_RCS_SENDER_RMO") + } return nil } diff --git a/db/sql/texts_by_senders.bob.go b/db/sql/texts_by_senders.bob.go new file mode 100644 index 00000000..3a8ce5bb --- /dev/null +++ b/db/sql/texts_by_senders.bob.go @@ -0,0 +1,135 @@ +// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package sql + +import ( + "context" + _ "embed" + "io" + "iter" + "time" + + enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/stephenafamo/bob" + "github.com/stephenafamo/bob/dialect/psql" + "github.com/stephenafamo/bob/dialect/psql/dialect" + "github.com/stephenafamo/bob/orm" + "github.com/stephenafamo/scan" +) + +//go:embed texts_by_senders.bob.sql +var formattedQueries_texts_by_senders string + +var textsBySendersSQL = formattedQueries_texts_by_senders[152:393] + +type TextsBySendersQuery = orm.ModQuery[*dialect.SelectQuery, textsBySenders, TextsBySendersRow, []TextsBySendersRow, textsBySendersTransformer] + +func TextsBySenders(Destination string, Source string) *TextsBySendersQuery { + var expressionTypArgs textsBySenders + + expressionTypArgs.Destination = psql.Arg(Destination) + expressionTypArgs.Source = psql.Arg(Source) + + return &TextsBySendersQuery{ + Query: orm.Query[textsBySenders, TextsBySendersRow, []TextsBySendersRow, textsBySendersTransformer]{ + ExecQuery: orm.ExecQuery[textsBySenders]{ + BaseQuery: bob.BaseQuery[textsBySenders]{ + Expression: expressionTypArgs, + Dialect: dialect.Dialect, + QueryType: bob.QueryTypeSelect, + }, + }, + Scanner: func(context.Context, []string) (func(*scan.Row) (any, error), func(any) (TextsBySendersRow, error)) { + return func(row *scan.Row) (any, error) { + var t TextsBySendersRow + row.ScheduleScanByIndex(0, &t.ID) + row.ScheduleScanByIndex(1, &t.Content) + row.ScheduleScanByIndex(2, &t.Created) + row.ScheduleScanByIndex(3, &t.Source) + row.ScheduleScanByIndex(4, &t.Destination) + row.ScheduleScanByIndex(5, &t.IsWelcome) + row.ScheduleScanByIndex(6, &t.Origin) + return &t, nil + }, func(v any) (TextsBySendersRow, error) { + return *(v.(*TextsBySendersRow)), nil + } + }, + }, + Mod: bob.ModFunc[*dialect.SelectQuery](func(q *dialect.SelectQuery) { + q.AppendSelect(expressionTypArgs.subExpr(12, 97)) + q.SetTable(expressionTypArgs.subExpr(108, 122)) + q.AppendWhere(expressionTypArgs.subExpr(135, 214)) + q.CombinedOrder.AppendOrder(expressionTypArgs.subExpr(230, 241)) + }), + } +} + +type TextsBySendersRow = struct { + ID int32 `db:"id"` + Content string `db:"content"` + Created time.Time `db:"created"` + Source string `db:"source"` + Destination string `db:"destination"` + IsWelcome bool `db:"is_welcome"` + Origin enums.CommsTextorigin `db:"origin"` +} + +type textsBySendersTransformer = bob.SliceTransformer[TextsBySendersRow, []TextsBySendersRow] + +type textsBySenders struct { + Destination bob.Expression + Source bob.Expression +} + +func (o textsBySenders) args() iter.Seq[orm.ArgWithPosition] { + return func(yield func(arg orm.ArgWithPosition) bool) { + if !yield(orm.ArgWithPosition{ + Name: "destination", + Start: 144, + Stop: 146, + Expression: o.Destination, + }) { + return + } + + if !yield(orm.ArgWithPosition{ + Name: "source", + Start: 165, + Stop: 167, + Expression: o.Source, + }) { + return + } + + if !yield(orm.ArgWithPosition{ + Name: "source", + Start: 191, + Stop: 193, + Expression: o.Source, + }) { + return + } + + if !yield(orm.ArgWithPosition{ + Name: "destination", + Start: 212, + Stop: 214, + Expression: o.Destination, + }) { + return + } + } +} + +func (o textsBySenders) raw(from, to int) string { + return textsBySendersSQL[from:to] +} + +func (o textsBySenders) subExpr(from, to int) bob.Expression { + return orm.ArgsToExpression(textsBySendersSQL, from, to, o.args()) +} + +func (o textsBySenders) WriteSQL(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { + return o.subExpr(0, len(textsBySendersSQL)).WriteSQL(ctx, w, d, start) +} diff --git a/db/sql/texts_by_senders.bob.sql b/db/sql/texts_by_senders.bob.sql new file mode 100644 index 00000000..fbe09f0c --- /dev/null +++ b/db/sql/texts_by_senders.bob.sql @@ -0,0 +1,20 @@ +-- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- This file is meant to be re-generated in place and/or deleted at any time. + +-- TextsBySenders +SELECT + id, + content, + created, + source, + destination, + is_welcome, + origin +FROM + comms.text_log +WHERE + (source = $1 AND destination = $2) + OR + (source = $3 AND destination = $4) +ORDER BY + created ASC; diff --git a/db/sql/texts_by_senders.sql b/db/sql/texts_by_senders.sql new file mode 100644 index 00000000..80fabedf --- /dev/null +++ b/db/sql/texts_by_senders.sql @@ -0,0 +1,17 @@ +-- TextsBySenders +SELECT + id, + content, + created, + source, + destination, + is_welcome, + origin +FROM + comms.text_log +WHERE + (source = $1 AND destination = $2) + OR + (source = $2 AND destination = $1) +ORDER BY + created ASC; diff --git a/go.mod b/go.mod index 00c6eb34..fdc15bd7 100644 --- a/go.mod +++ b/go.mod @@ -29,23 +29,31 @@ require ( github.com/tidwall/geojson v1.4.5 github.com/twilio/twilio-go v1.29.1 github.com/uber/h3-go/v4 v4.4.0 - golang.org/x/crypto v0.42.0 + golang.org/x/crypto v0.47.0 ) require ( github.com/ajg/form v1.5.1 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beevik/etree v1.1.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/golang/mock v1.6.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.2 // indirect github.com/klauspost/cpuid/v2 v2.2.11 // indirect github.com/klauspost/crc32 v1.3.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/maruel/genai v0.0.0-20251221000642-77279d1194c1 // indirect + github.com/maruel/httpjson v0.5.0 // indirect + github.com/maruel/roundtrippers v0.5.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/minio/crc64nvme v1.1.0 // indirect @@ -62,12 +70,14 @@ require ( github.com/tidwall/rtree v1.3.1 // indirect github.com/tidwall/sjson v1.2.4 // indirect github.com/tinylib/msgp v1.3.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + go.mau.fi/util v0.9.5 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 18176087..f17eabfd 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,14 @@ github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBV github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/alitto/pond/v2 v2.5.0 h1:vPzS5GnvSDRhWQidmj2djHllOmjFExVFbDGCw1jdqDw= github.com/alitto/pond/v2 v2.5.0/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -79,6 +85,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= @@ -94,6 +102,8 @@ github.com/jaswdr/faker/v2 v2.8.1 h1:2AcPgHDBXYQregFUH9LgVZKfFupc4SIquYhp29sf5wQ github.com/jaswdr/faker/v2 v2.8.1/go.mod h1:jZq+qzNQr8/P+5fHd9t3txe2GNPnthrTfohtnJ7B+68= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -115,8 +125,18 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/maruel/genai v0.0.0-20251221000642-77279d1194c1 h1:zBQI2oPYBnnho8AndgaD7ib8djNeTJZckwMOUGAqrn0= +github.com/maruel/genai v0.0.0-20251221000642-77279d1194c1/go.mod h1:5umBYxgRJHAjfc++Gto7w1Hnys5WHHHjwtkfiky5MSg= +github.com/maruel/httpjson v0.5.0 h1:fUkECNt2G2rSi9rzklMVcElsiucUj8LoKhKqaUvlaYA= +github.com/maruel/httpjson v0.5.0/go.mod h1:Rbue+VwOe1TC6doGXddW8EWg2fW4Je6RhCo7iPuNpTo= +github.com/maruel/roundtrippers v0.5.0 h1:0ot2VEWg2KbrHMh67/ysw5P9HQBhMdST4QZfR7QKFBo= +github.com/maruel/roundtrippers v0.5.0/go.mod h1:By9wgqtmfQEs7hQmz7m8N2jr2m8VDPXNIRxOtK/042U= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -245,10 +265,14 @@ github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfP github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM= github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mau.fi/util v0.9.5 h1:7AoWPCIZJGv4jvtFEuCe3GhAbI7uF9ckIooaXvwlIR4= +go.mau.fi/util v0.9.5/go.mod h1:g1uvZ03VQhtTt2BgaRGVytS/Zj67NV0YNIECch0sQCQ= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= @@ -267,8 +291,12 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -281,12 +309,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -303,6 +335,10 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -316,6 +352,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/llm/client.go b/llm/client.go new file mode 100644 index 00000000..45ff2a24 --- /dev/null +++ b/llm/client.go @@ -0,0 +1,22 @@ +package llm + +import ( + "github.com/rs/zerolog/log" +) + +type Message struct { + Content string + IsFromCustomer bool +} + +func GenerateNextMessage(history []Message, current Message) (Message, error) { + // In general our history + for i, msg := range history { + log.Info().Int("i", i).Bool("is_customer", msg.IsFromCustomer).Msg("History") + } + + return Message{ + Content: "hey there. :)", + IsFromCustomer: false, + }, nil +} diff --git a/llm/log.go b/llm/log.go new file mode 100644 index 00000000..52c48989 --- /dev/null +++ b/llm/log.go @@ -0,0 +1,42 @@ +package llm + +import ( + "log" + "strings" + "time" + + "github.com/rs/zerolog" + "go.mau.fi/util/exzerolog" +) + +type Logger = zerolog.Logger + +func createLogger() *Logger { + l := zerolog.New(zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { + //w.Out = io.Writer(buf) + w.TimeFormat = time.Stamp + })).With().Timestamp().Logger() + zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + exzerolog.SetupDefaults(&l) + return &l +} + +type ZerologWriter struct { + zerologger zerolog.Logger + level zerolog.Level +} + +func (w ZerologWriter) Write(p []byte) (n int, err error) { + msg := strings.TrimSuffix(string(p), "\n") + event := w.zerologger.WithLevel(w.level) + event.Msg(msg) + return len(p), nil +} + +func LoggerShim(l zerolog.Logger) *log.Logger { + writer := &ZerologWriter{ + zerologger: l, + level: zerolog.DebugLevel, + } + return log.New(writer, "", 0) +} diff --git a/llm/openai.go b/llm/openai.go new file mode 100644 index 00000000..d936bb0d --- /dev/null +++ b/llm/openai.go @@ -0,0 +1,175 @@ +package llm + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/maruel/genai" + "github.com/maruel/genai/adapters" + "github.com/maruel/genai/providers/openaichat" + "github.com/rs/zerolog/log" +) + +type openAIClient struct { + client *openaichat.Client + conversations map[string][]genai.Message + log *Logger +} + +var client *openAIClient + +type AIRequest struct { + Displayname string + Message string + Sender string + Timestamp time.Time +} + +func CreateOpenAIClient(ctx context.Context) error { + logger := createLogger() + + opts := genai.ProviderOptions{ + Model: genai.ModelCheap, + } + c, err := openaichat.New(ctx, &opts, nil) + if err != nil { + return fmt.Errorf("Failed to create genai client: %v", err) + } + client = &openAIClient{ + client: c, + conversations: make(map[string][]genai.Message), + log: logger, + } + return nil +} + +func (c *openAIClient) continueConversation(ctx context.Context, req AIRequest) error { + msgs, ok := c.conversations["roomid"] + if !ok { + msgs = genai.Messages{ + c.startConversation(ctx, req), + } + } else { + msgs = append(msgs, genai.NewTextMessage(fmt.Sprintf("(%s) user: %s\nbot: ", req.Timestamp.String(), req.Message))) + } + + c.log.Debug().Msg("Generating response...") + opts := genai.OptionsTools{ + Tools: []genai.ToolDef{ + { + Name: "followup_timer", + Description: "This should be used to indicate that the bot should follow up with the user in the future to check on task progress.", + Callback: func(ctx2 context.Context, input *FollowupTimerInput) (string, error) { + return c.followupSchedule(ctx2, req, input) + }, + }, { + Name: "switch_task", + Description: "Any time the user indicates they change tasks this must be called to update the record of what tasks are being done.", + Callback: func(ctx2 context.Context, input *SwitchTaskInput) (string, error) { + return c.switchTask(ctx2, req, input) + }, + }, + }, + } + + res, _, err := adapters.GenSyncWithToolCallLoop(ctx, c.client, msgs, &opts) + if err != nil { + return fmt.Errorf("Failed to continue conversation: %v", err) + } + + for _, m := range res { + msgs = append(msgs, m) + // Empty responses are tool call related. + if m.String() == "" { + } else { + //c.log.Info().Str("room", req.RoomID.String()).Msg(m.String()) + var toSay string = m.String() + toSay = strings.Replace(toSay, "bot: ", "", 1) + log.Info().Str("to say", toSay).Msg("Responding") + /*c.aiResponseChannel <- AIResponse{ + Message: toSay, + RoomID: req.RoomID, + }*/ + } + } + //c.conversations[req.RoomID.String()] = msgs + + return nil +} + +type FollowupTimerInput struct { + DelayInSeconds int64 `json:"delay_in_seconds"` +} + +func (c *openAIClient) followupFire(ctx context.Context, req AIRequest, duration time.Duration) { + if err := ctx.Err(); err != nil { + //c.log.Info().Str("room", req.RoomID.String()).Msg("Context canceled") + return + } + msgs, ok := c.conversations["roomid"] + if !ok { + //c.log.Warn().Str("room", req.RoomID.String()).Str("elapsed", duration.String()).Msg("No messages for room") + return + } + msgs = append(msgs, genai.NewTextMessage(fmt.Sprintf("<%s passed>", duration.String()))) + res, err := c.client.GenSync(ctx, msgs) + if err != nil { + //c.log.Error().Str("room", req.RoomID.String()).Err(err).Msg("Failed to continue after timer") + return + } + msgs = append(msgs, res.Message) + var toSay string = res.String() + toSay = strings.Replace(toSay, "bot: ", "", 1) + log.Info().Str("to say", toSay).Msg("To say") + /*c.aiResponseChannel <- AIResponse{ + Message: toSay, + RoomID: req.RoomID, + } + c.conversations[req.RoomID.String()] = msgs + */ +} + +func (c *openAIClient) followupSchedule(ctx context.Context, req AIRequest, input *FollowupTimerInput) (string, error) { + //c.log.Info().Str("room", req.RoomID.String()).Int64("delay", input.DelayInSeconds).Msg("Followup timer scheduled.") + duration, err := time.ParseDuration(fmt.Sprintf("%ds", input.DelayInSeconds)) + if err != nil { + return "", fmt.Errorf("Failed to parse %d as a valid duration: %v", input.DelayInSeconds, err) + } + /*c.aiResponseChannel <- AIResponse{ + Message: fmt.Sprintf("⌛ followup scheduled '%s'", duration.String()), + RoomID: req.RoomID, + }*/ + time.AfterFunc(duration, func() { + c.followupFire(ctx, req, duration) + }) + return fmt.Sprintf("Followup timer set for %s in the future", duration.String()), nil +} + +type SwitchTaskInput struct { + TaskName string `json:"task_name"` +} + +func (c *openAIClient) switchTask(ctx context.Context, req AIRequest, input *SwitchTaskInput) (string, error) { + //c.log.Info().Str("room", req.RoomID.String()).Str("task", input.TaskName).Msg("Task Switched") + /*c.aiResponseChannel <- AIResponse{ + Message: fmt.Sprintf("📋 notes task '%s'", input.TaskName), + RoomID: req.RoomID, + }*/ + + return fmt.Sprintf("Recorded a switch to task %s at %s", input.TaskName, time.Now().String()), nil +} + +func (c *openAIClient) startConversation(ctx context.Context, req AIRequest) genai.Message { + return genai.NewTextMessage(fmt.Sprintf( + `This is a text chat conversation between an employee and a chatbot helping to manage timecards. + The user's name is '%[1]s'. + Messages from the user will start with '(timestamp) %[1]s:'. + Messages from the bot will start with 'bot:'. + Sometimes the user won't say anything for a long time and the chatbot needs to follow-up with them. + When time passes, there will be a prompt like '<200s passed>'. + The bot should then prompt the user to provide a bit of information about what they've been working on during that time. + The bot should be interested to know what the user's goals are at a high level and should pay attention to any difficulties or frustrations the user experiences.\n\n + (%[2]s) user: %[3]s\nbot:`, req.Displayname, req.Timestamp.String(), req.Message)) +} diff --git a/main.go b/main.go index ac76e6af..ee3b1c8c 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/llm" "github.com/Gleipnir-Technology/nidus-sync/public-report" nidussync "github.com/Gleipnir-Technology/nidus-sync/sync" "github.com/go-chi/chi/v5" @@ -52,6 +53,7 @@ func main() { log.Error().Err(err).Msg("Failed to store text source phone numbers") os.Exit(4) } + router_logger := log.With().Logger() r := chi.NewRouter() @@ -75,6 +77,11 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + err = llm.CreateOpenAIClient(ctx) + if err != nil { + log.Error().Err(err).Msg("Failed to start openAI client") + os.Exit(5) + } background.Start(ctx) server := &http.Server{ Addr: config.Bind, diff --git a/platform/text.go b/platform/text.go new file mode 100644 index 00000000..776fe315 --- /dev/null +++ b/platform/text.go @@ -0,0 +1,113 @@ +package platform + +import ( + "context" + "fmt" + "strings" + + "github.com/Gleipnir-Technology/nidus-sync/comms/text" + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/Gleipnir-Technology/nidus-sync/db/sql" + "github.com/Gleipnir-Technology/nidus-sync/llm" + "github.com/rs/zerolog/log" +) + +// Translate from Twilio's representation of a RCS message sender to our concept of a phone number +// From: rcs:dev_report_mosquitoes_online_dosrvwxm_agent +// To: +16235525879 +func getDst(ctx context.Context, to string) (string, error) { + + if to == config.TwilioRCSSenderRMO { + return config.PhoneNumberReportStr, nil + } + /* + phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, to) + if err != nil { + return "", fmt.Errorf("Failed to search for dest phone %s: %w", to, err) + } + return phone.E164, nil + */ + return "", fmt.Errorf("Cannot match phone number to '%s'", to) +} + +func loadPreviousMessages(ctx context.Context, dst, src string) ([]llm.Message, error) { + messages, err := sql.TextsBySenders(dst, src).All(ctx, db.PGInstance.BobDB) + results := make([]llm.Message, 0) + if err != nil { + return results, fmt.Errorf("Failed to get message history for %s and %s: %w", dst, src, err) + } + log.Info().Int("count", len(messages)).Str("src", src).Str("dst", dst).Msg("Found previous messages") + for _, m := range messages { + is_from_customer := (m.Source == src) + results = append(results, llm.Message{ + IsFromCustomer: is_from_customer, + Content: m.Content, + }) + } + return results, nil +} + +func splitPhoneSource(s string) (string, string) { + parts := strings.Split(s, ":") + switch len(parts) { + case 0: + return "this isn't", "possible" + case 1: + return "", s + case 2: + return parts[0], parts[1] + default: + log.Warn().Str("s", s).Msg("Got an incomprehensible number of parts of a phone number") + return parts[0], parts[1] + } + +} + +func isSubscribed(ctx context.Context, src string) (bool, error) { + phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src) + if err != nil { + return false, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) + } + return phone.IsSubscribed, nil +} + +func HandleTextMessage(from string, to string, body string) { + ctx := context.Background() + type_, src := splitPhoneSource(from) + dst, err := getDst(ctx, to) + if err != nil { + log.Error().Err(err).Str("to", to).Msg("Failed to get dst") + return + } + subscribed, err := isSubscribed(ctx, from) + if err != nil { + log.Error().Err(err).Msg("Failed to handle message") + return + } + if !subscribed { + err = text.SendInitialReprompt(ctx, dst, src) + if err != nil { + log.Error().Err(err).Msg("Failed to resend initial prompt.") + } + return + } + previous_messages, err := loadPreviousMessages(ctx, dst, src) + if err != nil { + log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages") + return + } + current := llm.Message{ + Content: body, + IsFromCustomer: true, + } + log.Info().Int("len", len(previous_messages)).Msg("passing") + next_message, err := llm.GenerateNextMessage(previous_messages, current) + if err != nil { + log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to generate next message") + return + } + text.SendTextFromLLM(next_message.Content) + log.Info().Str("from", from).Str("from-type", type_).Str("to", to).Str("src", src).Str("dst", dst).Str("body", body).Str("reply", next_message.Content).Msg("Handling text message") +} diff --git a/tools/texts_by_senders.sql b/tools/texts_by_senders.sql new file mode 100644 index 00000000..a60a8ebe --- /dev/null +++ b/tools/texts_by_senders.sql @@ -0,0 +1,17 @@ +-- TextsBySenders +SELECT + id, + content, + created, + source, + destination, + is_welcome, + origin +FROM + comms.text_log +WHERE + (source = :src AND destination = :dst) + OR + (source = :dst AND destination = :src) +ORDER BY + created ASC; From c0ecfe2e1808423fdc9de544cf1eb8c5318dbbcf Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 20:41:21 +0000 Subject: [PATCH 0140/1513] Add more log info on login failure --- api/signin.go | 1 + 1 file changed, 1 insertion(+) diff --git a/api/signin.go b/api/signin.go index d182c897..bd319adc 100644 --- a/api/signin.go +++ b/api/signin.go @@ -37,6 +37,7 @@ func postSignin(w http.ResponseWriter, r *http.Request) { http.Error(w, "invalid-credentials", http.StatusUnauthorized) return } + log.Error().Err(err).Str("username", username).Msg("Login server error") http.Error(w, "signin-server-error", http.StatusInternalServerError) return } From d2d95a1f6bdc68cf1ab745b431109476b10d08d3 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 20:51:07 +0000 Subject: [PATCH 0141/1513] Update vendor hash for recent vendor changes --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index eedd831e..3d6ed30c 100644 --- a/default.nix +++ b/default.nix @@ -9,5 +9,5 @@ pkgs.buildGoModule rec { subPackages = []; version = "0.0.11"; # Needs to be updated after every modification of go.mod/go.sum - vendorHash = "sha256-Z1kpPmQytPAaKXQDlD8ILNyPchZornVTO+enOcS2cEQ="; + vendorHash = "sha256-HJg9c+ZUWxQgu5FBgcc0BMUWkkK6YyFVVBrFF0oUz/8="; } From 1cd4a31404f873e20c640483c58039a0162dc079 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 20:51:26 +0000 Subject: [PATCH 0142/1513] Start saving subscribed status --- platform/text.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/platform/text.go b/platform/text.go index 776fe315..2157dae2 100644 --- a/platform/text.go +++ b/platform/text.go @@ -11,6 +11,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/sql" "github.com/Gleipnir-Technology/nidus-sync/llm" + "github.com/aarondl/opt/omit" "github.com/rs/zerolog/log" ) @@ -73,6 +74,17 @@ func isSubscribed(ctx context.Context, src string) (bool, error) { return phone.IsSubscribed, nil } +func setSubscribed(ctx context.Context, src string, is_subscribed bool) error { + phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src) + if err != nil { + return fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) + } + phone.Update(ctx, db.PGInstance.BobDB, &models.CommsPhoneSetter{ + IsSubscribed: omit.From(is_subscribed), + }) + return nil +} + func HandleTextMessage(from string, to string, body string) { ctx := context.Background() type_, src := splitPhoneSource(from) @@ -81,12 +93,17 @@ func HandleTextMessage(from string, to string, body string) { log.Error().Err(err).Str("to", to).Msg("Failed to get dst") return } - subscribed, err := isSubscribed(ctx, from) + subscribed, err := isSubscribed(ctx, src) if err != nil { log.Error().Err(err).Msg("Failed to handle message") return } if !subscribed { + body_l := strings.TrimSpace(strings.ToLower(body)) + if body_l == "stop" { + setSubscribed(ctx, src, false) + return + } err = text.SendInitialReprompt(ctx, dst, src) if err != nil { log.Error().Err(err).Msg("Failed to resend initial prompt.") From e8e840ec44e47e0d4c9c539730b8c11c4ece6dab Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 21:11:31 +0000 Subject: [PATCH 0143/1513] Make username unique, make is_subscribed nullable --- comms/text/db.go | 13 +----- comms/text/report-subscription.go | 46 ++++++++++--------- db/dberrors/user_.bob.go | 9 ++++ db/dbinfo/comms.phone.bob.go | 4 +- db/dbinfo/user_.bob.go | 37 +++++++++++++-- db/factory/bobfactory_main.bob.go | 2 +- db/factory/comms.phone.bob.go | 42 ++++++++++++----- .../00042_text_subscribe_nullable.sql | 2 + db/migrations/00043_username_unique.sql | 2 + db/models/comms.phone.bob.go | 26 ++++++----- platform/text.go | 40 +++++++++++----- 11 files changed, 149 insertions(+), 74 deletions(-) create mode 100644 db/migrations/00042_text_subscribe_nullable.sql create mode 100644 db/migrations/00043_username_unique.sql diff --git a/comms/text/db.go b/comms/text/db.go index 9801e1ca..791dac38 100644 --- a/comms/text/db.go +++ b/comms/text/db.go @@ -15,6 +15,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/nyaruka/phonenumbers" "github.com/rs/zerolog/log" "github.com/stephenafamo/bob/types/pgtypes" @@ -55,7 +56,7 @@ func ensureInDB(ctx context.Context, destination string) (err error) { if err.Error() == "sql: no rows in result set" { _, err = models.CommsPhones.Insert(&models.CommsPhoneSetter{ E164: omit.From(destination), - IsSubscribed: omit.From(false), + IsSubscribed: omitnull.FromPtr[bool](nil), }).One(ctx, db.PGInstance.BobDB) if err != nil { return fmt.Errorf("Failed to insert new phone contact: %w", err) @@ -81,16 +82,6 @@ func insertTextLog(ctx context.Context, content string, destination string, sour return err } -func isSubscribed(ctx context.Context, destination string) (bool, error) { - phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, destination) - if err != nil { - if err.Error() == "sql: no rows in result set" { - return false, nil - } - return false, fmt.Errorf("Failed to find phone number %s: %w", destination, err) - } - return phone.IsSubscribed, nil -} func generatePublicId(t enums.CommsMessagetypeemail, m map[string]string) string { if m == nil || len(m) == 0 { diff --git a/comms/text/report-subscription.go b/comms/text/report-subscription.go index c43f6b89..12328338 100644 --- a/comms/text/report-subscription.go +++ b/comms/text/report-subscription.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/Gleipnir-Technology/nidus-sync/db/enums" + //"github.com/Gleipnir-Technology/nidus-sync/db/enums" + //"github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/nyaruka/phonenumbers" //"github.com/rs/zerolog/log" ) @@ -43,29 +44,32 @@ func (j jobReportSubscription) source() string { } func sendReportSubscription(ctx context.Context, job Job) error { - j, ok := job.(jobReportSubscription) - if !ok { - return fmt.Errorf("job is not for report subscription confirmation") - } + /* + j, ok := job.(jobReportSubscription) + if !ok { + return fmt.Errorf("job is not for report subscription confirmation") + } - sub, err := isSubscribed(ctx, job.destination()) - if err != nil { - return fmt.Errorf("Failed to check if subscribed: %w", err) - } - if !sub { - err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction, false) + sub, err := isSubscribed(ctx, job.destination()) if err != nil { - return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + return fmt.Errorf("Failed to check if subscribed: %w", err) } - } else { - err = delayMessage(ctx, j.source(), j.destination(), j.content(), enums.CommsTextjobtypeReportConfirmation) - if err != nil { - return fmt.Errorf("Failed to delay report subscription message: %w", err) + if !sub { + err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction, false) + if err != nil { + return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + } + } else { + err = delayMessage(ctx, j.source(), j.destination(), j.content(), enums.CommsTextjobtypeReportConfirmation) + if err != nil { + return fmt.Errorf("Failed to delay report subscription message: %w", err) + } + err := ensureInitialText(ctx, j.source(), j.destination()) + if err != nil { + return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) + } } - err := ensureInitialText(ctx, j.source(), j.destination()) - if err != nil { - return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) - } - } + return nil + */ return nil } diff --git a/db/dberrors/user_.bob.go b/db/dberrors/user_.bob.go index d000ac1f..7f08ae62 100644 --- a/db/dberrors/user_.bob.go +++ b/db/dberrors/user_.bob.go @@ -10,8 +10,17 @@ var UserErrors = &userErrors{ columns: []string{"id"}, s: "user__pkey", }, + + ErrUniqueUserUsernameUnique: &UniqueConstraintError{ + schema: "", + table: "user_", + columns: []string{"username"}, + s: "user_username_unique", + }, } type userErrors struct { ErrUniqueUser_Pkey *UniqueConstraintError + + ErrUniqueUserUsernameUnique *UniqueConstraintError } diff --git a/db/dbinfo/comms.phone.bob.go b/db/dbinfo/comms.phone.bob.go index 2919387c..be6039b2 100644 --- a/db/dbinfo/comms.phone.bob.go +++ b/db/dbinfo/comms.phone.bob.go @@ -27,9 +27,9 @@ var CommsPhones = Table[ IsSubscribed: column{ Name: "is_subscribed", DBType: "boolean", - Default: "", + Default: "NULL", Comment: "", - Nullable: false, + Nullable: true, Generated: false, AutoIncr: false, }, diff --git a/db/dbinfo/user_.bob.go b/db/dbinfo/user_.bob.go index b72cfaf4..1c1f25ef 100644 --- a/db/dbinfo/user_.bob.go +++ b/db/dbinfo/user_.bob.go @@ -142,6 +142,23 @@ var Users = Table[ Where: "", Include: []string{}, }, + UserUsernameUnique: index{ + Type: "btree", + Name: "user_username_unique", + Columns: []indexColumn{ + { + Name: "username", + Desc: null.FromCond(false, true), + IsExpression: false, + }, + }, + Unique: true, + Comment: "", + NullsFirst: []bool{false}, + NullsDistinct: false, + Where: "", + Include: []string{}, + }, }, PrimaryKey: &constraint{ Name: "user__pkey", @@ -159,6 +176,13 @@ var Users = Table[ ForeignColumns: []string{"id"}, }, }, + Uniques: userUniques{ + UserUsernameUnique: constraint{ + Name: "user_username_unique", + Columns: []string{"username"}, + Comment: "", + }, + }, Comment: "", } @@ -185,12 +209,13 @@ func (c userColumns) AsSlice() []column { } type userIndexes struct { - UserPkey index + UserPkey index + UserUsernameUnique index } func (i userIndexes) AsSlice() []index { return []index{ - i.UserPkey, + i.UserPkey, i.UserUsernameUnique, } } @@ -204,10 +229,14 @@ func (f userForeignKeys) AsSlice() []foreignKey { } } -type userUniques struct{} +type userUniques struct { + UserUsernameUnique constraint +} func (u userUniques) AsSlice() []constraint { - return []constraint{} + return []constraint{ + u.UserUsernameUnique, + } } type userChecks struct{} diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index 6e8d4391..45b2b33f 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -293,7 +293,7 @@ func (f *Factory) FromExistingCommsPhone(m *models.CommsPhone) *CommsPhoneTempla o := &CommsPhoneTemplate{f: f, alreadyPersisted: true} o.E164 = func() string { return m.E164 } - o.IsSubscribed = func() bool { return m.IsSubscribed } + o.IsSubscribed = func() null.Val[bool] { return m.IsSubscribed } ctx := context.Background() if len(m.R.DestinationTextJobs) > 0 { diff --git a/db/factory/comms.phone.bob.go b/db/factory/comms.phone.bob.go index cefbb124..abcd554d 100644 --- a/db/factory/comms.phone.bob.go +++ b/db/factory/comms.phone.bob.go @@ -8,7 +8,9 @@ import ( "testing" models "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" "github.com/stephenafamo/bob" ) @@ -35,7 +37,7 @@ func (mods CommsPhoneModSlice) Apply(ctx context.Context, n *CommsPhoneTemplate) // all columns are optional and should be set by mods type CommsPhoneTemplate struct { E164 func() string - IsSubscribed func() bool + IsSubscribed func() null.Val[bool] r commsPhoneR f *Factory @@ -123,7 +125,7 @@ func (o CommsPhoneTemplate) BuildSetter() *models.CommsPhoneSetter { } if o.IsSubscribed != nil { val := o.IsSubscribed() - m.IsSubscribed = omit.From(val) + m.IsSubscribed = omitnull.FromNull(val) } return m @@ -177,10 +179,6 @@ func ensureCreatableCommsPhone(m *models.CommsPhoneSetter) { val := random_string(nil) m.E164 = omit.From(val) } - if !(m.IsSubscribed.IsValue()) { - val := random_bool(nil) - m.IsSubscribed = omit.From(val) - } } // insertOptRels creates and inserts any optional the relationships on *models.CommsPhone @@ -378,14 +376,14 @@ func (m commsPhoneMods) RandomE164(f *faker.Faker) CommsPhoneMod { } // Set the model columns to this value -func (m commsPhoneMods) IsSubscribed(val bool) CommsPhoneMod { +func (m commsPhoneMods) IsSubscribed(val null.Val[bool]) CommsPhoneMod { return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) { - o.IsSubscribed = func() bool { return val } + o.IsSubscribed = func() null.Val[bool] { return val } }) } // Set the Column from the function -func (m commsPhoneMods) IsSubscribedFunc(f func() bool) CommsPhoneMod { +func (m commsPhoneMods) IsSubscribedFunc(f func() null.Val[bool]) CommsPhoneMod { return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) { o.IsSubscribed = f }) @@ -400,10 +398,32 @@ func (m commsPhoneMods) UnsetIsSubscribed() CommsPhoneMod { // Generates a random value for the column using the given faker // if faker is nil, a default faker is used +// The generated value is sometimes null func (m commsPhoneMods) RandomIsSubscribed(f *faker.Faker) CommsPhoneMod { return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) { - o.IsSubscribed = func() bool { - return random_bool(f) + o.IsSubscribed = func() null.Val[bool] { + if f == nil { + f = &defaultFaker + } + + val := random_bool(f) + return null.From(val) + } + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +// The generated value is never null +func (m commsPhoneMods) RandomIsSubscribedNotNull(f *faker.Faker) CommsPhoneMod { + return CommsPhoneModFunc(func(_ context.Context, o *CommsPhoneTemplate) { + o.IsSubscribed = func() null.Val[bool] { + if f == nil { + f = &defaultFaker + } + + val := random_bool(f) + return null.From(val) } }) } diff --git a/db/migrations/00042_text_subscribe_nullable.sql b/db/migrations/00042_text_subscribe_nullable.sql new file mode 100644 index 00000000..778c7577 --- /dev/null +++ b/db/migrations/00042_text_subscribe_nullable.sql @@ -0,0 +1,2 @@ +-- +goose Up +ALTER TABLE comms.phone ALTER COLUMN is_subscribed DROP NOT NULL; diff --git a/db/migrations/00043_username_unique.sql b/db/migrations/00043_username_unique.sql new file mode 100644 index 00000000..48225744 --- /dev/null +++ b/db/migrations/00043_username_unique.sql @@ -0,0 +1,2 @@ +-- +goose Up +ALTER TABLE user_ ADD CONSTRAINT user_username_unique UNIQUE (username); diff --git a/db/models/comms.phone.bob.go b/db/models/comms.phone.bob.go index 6af2d6e7..56a665c0 100644 --- a/db/models/comms.phone.bob.go +++ b/db/models/comms.phone.bob.go @@ -8,7 +8,9 @@ import ( "fmt" "io" + "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/stephenafamo/bob" "github.com/stephenafamo/bob/dialect/psql" "github.com/stephenafamo/bob/dialect/psql/dialect" @@ -23,8 +25,8 @@ import ( // CommsPhone is an object representing the database table. type CommsPhone struct { - E164 string `db:"e164,pk" ` - IsSubscribed bool `db:"is_subscribed" ` + E164 string `db:"e164,pk" ` + IsSubscribed null.Val[bool] `db:"is_subscribed" ` R commsPhoneR `db:"-" ` @@ -78,8 +80,8 @@ func (commsPhoneColumns) AliasedAs(alias string) commsPhoneColumns { // All values are optional, and do not have to be set // Generated columns are not included type CommsPhoneSetter struct { - E164 omit.Val[string] `db:"e164,pk" ` - IsSubscribed omit.Val[bool] `db:"is_subscribed" ` + E164 omit.Val[string] `db:"e164,pk" ` + IsSubscribed omitnull.Val[bool] `db:"is_subscribed" ` } func (s CommsPhoneSetter) SetColumns() []string { @@ -87,7 +89,7 @@ func (s CommsPhoneSetter) SetColumns() []string { if s.E164.IsValue() { vals = append(vals, "e164") } - if s.IsSubscribed.IsValue() { + if !s.IsSubscribed.IsUnset() { vals = append(vals, "is_subscribed") } return vals @@ -97,8 +99,8 @@ func (s CommsPhoneSetter) Overwrite(t *CommsPhone) { if s.E164.IsValue() { t.E164 = s.E164.MustGet() } - if s.IsSubscribed.IsValue() { - t.IsSubscribed = s.IsSubscribed.MustGet() + if !s.IsSubscribed.IsUnset() { + t.IsSubscribed = s.IsSubscribed.MustGetNull() } } @@ -115,8 +117,8 @@ func (s *CommsPhoneSetter) Apply(q *dialect.InsertQuery) { vals[0] = psql.Raw("DEFAULT") } - if s.IsSubscribed.IsValue() { - vals[1] = psql.Arg(s.IsSubscribed.MustGet()) + if !s.IsSubscribed.IsUnset() { + vals[1] = psql.Arg(s.IsSubscribed.MustGetNull()) } else { vals[1] = psql.Raw("DEFAULT") } @@ -139,7 +141,7 @@ func (s CommsPhoneSetter) Expressions(prefix ...string) []bob.Expression { }}) } - if s.IsSubscribed.IsValue() { + if !s.IsSubscribed.IsUnset() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "is_subscribed")...), psql.Arg(s.IsSubscribed), @@ -650,7 +652,7 @@ func (commsPhone0 *CommsPhone) AttachSourceTextLogs(ctx context.Context, exec bo type commsPhoneWhere[Q psql.Filterable] struct { E164 psql.WhereMod[Q, string] - IsSubscribed psql.WhereMod[Q, bool] + IsSubscribed psql.WhereNullMod[Q, bool] } func (commsPhoneWhere[Q]) AliasedAs(alias string) commsPhoneWhere[Q] { @@ -660,7 +662,7 @@ func (commsPhoneWhere[Q]) AliasedAs(alias string) commsPhoneWhere[Q] { func buildCommsPhoneWhere[Q psql.Filterable](cols commsPhoneColumns) commsPhoneWhere[Q] { return commsPhoneWhere[Q]{ E164: psql.Where[Q, string](cols.E164), - IsSubscribed: psql.Where[Q, bool](cols.IsSubscribed), + IsSubscribed: psql.WhereNull[Q, bool](cols.IsSubscribed), } } diff --git a/platform/text.go b/platform/text.go index 2157dae2..e857a702 100644 --- a/platform/text.go +++ b/platform/text.go @@ -11,7 +11,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/sql" "github.com/Gleipnir-Technology/nidus-sync/llm" - "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" ) @@ -66,12 +66,16 @@ func splitPhoneSource(s string) (string, string) { } -func isSubscribed(ctx context.Context, src string) (bool, error) { +func isSubscribed(ctx context.Context, src string) (*bool, error) { phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src) if err != nil { - return false, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) + return nil, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) } - return phone.IsSubscribed, nil + if phone.IsSubscribed.IsNull() { + return nil, nil + } + result := phone.IsSubscribed.MustGet() + return &result, nil } func setSubscribed(ctx context.Context, src string, is_subscribed bool) error { @@ -80,11 +84,16 @@ func setSubscribed(ctx context.Context, src string, is_subscribed bool) error { return fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) } phone.Update(ctx, db.PGInstance.BobDB, &models.CommsPhoneSetter{ - IsSubscribed: omit.From(is_subscribed), + IsSubscribed: omitnull.From(is_subscribed), }) + log.Info().Str("src", src).Bool("is_subscribed", is_subscribed).Msg("Set number subscribed") return nil } +func handleWaitingTextJobs(ctx context.Context, src string) { + log.Info().Str("src", src).Msg("Pretend handle waiting jobs") + +} func HandleTextMessage(from string, to string, body string) { ctx := context.Background() type_, src := splitPhoneSource(from) @@ -98,18 +107,25 @@ func HandleTextMessage(from string, to string, body string) { log.Error().Err(err).Msg("Failed to handle message") return } - if !subscribed { + // We don't know if they're subscribed or not. + if subscribed == nil { body_l := strings.TrimSpace(strings.ToLower(body)) - if body_l == "stop" { + switch body_l { + case "stop": setSubscribed(ctx, src, false) - return - } - err = text.SendInitialReprompt(ctx, dst, src) - if err != nil { - log.Error().Err(err).Msg("Failed to resend initial prompt.") + case "yes": + setSubscribed(ctx, src, true) + handleWaitingTextJobs(ctx, src) + default: + err = text.SendInitialReprompt(ctx, dst, src) + if err != nil { + log.Error().Err(err).Msg("Failed to resend initial prompt.") + } } return } + if !(*subscribed) { + } previous_messages, err := loadPreviousMessages(ctx, dst, src) if err != nil { log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages") From 407b4786373ed5083898a1fe4f543e53adf69845 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 26 Jan 2026 21:21:21 +0000 Subject: [PATCH 0144/1513] Fold more text logic into the platform Because it is better at managing the database, the comms/text package will just be for integration. --- comms/text/db.go | 63 ---------------------------- comms/text/initial.go | 37 ---------------- comms/text/text.go | 11 +---- main.go | 4 +- platform/text.go | 98 +++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 98 insertions(+), 115 deletions(-) delete mode 100644 comms/text/initial.go diff --git a/comms/text/db.go b/comms/text/db.go index 791dac38..cd6c3d7d 100644 --- a/comms/text/db.go +++ b/comms/text/db.go @@ -1,31 +1,17 @@ package text import ( - "context" "crypto/sha256" "database/sql" "encoding/hex" "fmt" "sort" "strings" - "time" - "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" - "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/aarondl/opt/omit" - "github.com/aarondl/opt/omitnull" - "github.com/nyaruka/phonenumbers" - "github.com/rs/zerolog/log" "github.com/stephenafamo/bob/types/pgtypes" ) -func StoreSources() error { - ctx := context.TODO() - src := phonenumbers.Format(&config.PhoneNumberReport, phonenumbers.E164) - return ensureInDB(ctx, src) -} func convertToPGData(data map[string]string) pgtypes.HStore { result := pgtypes.HStore{} for k, v := range data { @@ -34,55 +20,6 @@ func convertToPGData(data map[string]string) pgtypes.HStore { return result } -func delayMessage(ctx context.Context, source string, destination string, content string, type_ enums.CommsTextjobtype) error { - job, err := models.CommsTextJobs.Insert(&models.CommsTextJobSetter{ - Content: omit.From(content), - Created: omit.From(time.Now()), - Destination: omit.From(destination), - //ID: - Type: omit.From(type_), - }).One(ctx, db.PGInstance.BobDB) - if err != nil { - return fmt.Errorf("Failed to add delayed text job: %w", err) - } - log.Info().Int32("id", job.ID).Msg("Created delayed text job") - return nil -} - -func ensureInDB(ctx context.Context, destination string) (err error) { - _, err = models.FindCommsPhone(ctx, db.PGInstance.BobDB, destination) - if err != nil { - // doesn't exist - if err.Error() == "sql: no rows in result set" { - _, err = models.CommsPhones.Insert(&models.CommsPhoneSetter{ - E164: omit.From(destination), - IsSubscribed: omitnull.FromPtr[bool](nil), - }).One(ctx, db.PGInstance.BobDB) - if err != nil { - return fmt.Errorf("Failed to insert new phone contact: %w", err) - } - log.Info().Str("phone", destination).Msg("Added text to the comms database") - return nil - } - return fmt.Errorf("Unexpected error searching for phone contact: %w", err) - } - return nil -} - -func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool) (err error) { - _, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ - //ID: - Content: omit.From(content), - Created: omit.From(time.Now()), - Destination: omit.From(destination), - IsWelcome: omit.From(is_welcome), - Origin: omit.From(origin), - Source: omit.From(source), - }).One(ctx, db.PGInstance.BobDB) - - return err -} - func generatePublicId(t enums.CommsMessagetypeemail, m map[string]string) string { if m == nil || len(m) == 0 { // Return hash of empty string for empty maps diff --git a/comms/text/initial.go b/comms/text/initial.go deleted file mode 100644 index 0c25a62c..00000000 --- a/comms/text/initial.go +++ /dev/null @@ -1,37 +0,0 @@ -package text - -import ( - "context" - "fmt" - - "github.com/Gleipnir-Technology/nidus-sync/db" - "github.com/Gleipnir-Technology/nidus-sync/db/enums" - "github.com/Gleipnir-Technology/nidus-sync/db/models" -) - -func ensureInitialText(ctx context.Context, src string, dst string) error { - // - origin := enums.CommsTextoriginWebsiteAction - rows, err := models.CommsTextLogs.Query( - models.SelectWhere.CommsTextLogs.Destination.EQ(dst), - models.SelectWhere.CommsTextLogs.IsWelcome.EQ(true), - ).All(ctx, db.PGInstance.BobDB) - if err != nil { - return fmt.Errorf("Failed to query text logs: %w", err) - } - if len(rows) > 0 { - return nil - } - content := "Welcome to Report Mosquitoes Online. We received your request and want to confirm text updates. Reply YES to continue. Reply STOP at any time to unsubscribe" - err = sendText(ctx, src, dst, content, origin, true) - if err != nil { - return fmt.Errorf("Failed to send initial confirmation: %w", err) - } - return nil -} - -func SendInitialReprompt(ctx context.Context, src string, dst string) error { - content := "I have to start with either 'YES' or 'STOP' first, Which do you want?" - err := sendText(ctx, src, dst, content, enums.CommsTextoriginLLM, false) - return err -} diff --git a/comms/text/text.go b/comms/text/text.go index 8384405f..34d2aa7a 100644 --- a/comms/text/text.go +++ b/comms/text/text.go @@ -6,7 +6,6 @@ import ( "fmt" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/nyaruka/phonenumbers" "github.com/rs/zerolog/log" "github.com/twilio/twilio-go" @@ -19,15 +18,7 @@ func ParsePhoneNumber(input string) (*E164, error) { return phonenumbers.Parse(input, "US") } -func sendText(ctx context.Context, source string, destination string, message string, origin enums.CommsTextorigin, is_welcome bool) error { - err := ensureInDB(ctx, destination) - if err != nil { - return fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err) - } - err = insertTextLog(ctx, message, destination, source, origin, is_welcome) - if err != nil { - return fmt.Errorf("Failed to insert text message in the DB: %w", err) - } +func SendText(ctx context.Context, source string, destination string, message string) error { client := twilio.NewRestClient() params := &twilioApi.CreateMessageParams{} diff --git a/main.go b/main.go index ee3b1c8c..012f9372 100644 --- a/main.go +++ b/main.go @@ -13,10 +13,10 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/background" "github.com/Gleipnir-Technology/nidus-sync/comms/email" - "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/llm" + "github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/Gleipnir-Technology/nidus-sync/public-report" nidussync "github.com/Gleipnir-Technology/nidus-sync/sync" "github.com/go-chi/chi/v5" @@ -48,7 +48,7 @@ func main() { os.Exit(3) } - err = text.StoreSources() + err = platform.TextStoreSources() if err != nil { log.Error().Err(err).Msg("Failed to store text source phone numbers") os.Exit(4) diff --git a/platform/text.go b/platform/text.go index e857a702..f77a6e77 100644 --- a/platform/text.go +++ b/platform/text.go @@ -4,17 +4,97 @@ import ( "context" "fmt" "strings" + "time" "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/sql" "github.com/Gleipnir-Technology/nidus-sync/llm" + "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" + "github.com/nyaruka/phonenumbers" "github.com/rs/zerolog/log" ) +func TextStoreSources() error { + ctx := context.TODO() + src := phonenumbers.Format(&config.PhoneNumberReport, phonenumbers.E164) + return ensureInDB(ctx, src) +} + +func delayMessage(ctx context.Context, source string, destination string, content string, type_ enums.CommsTextjobtype) error { + job, err := models.CommsTextJobs.Insert(&models.CommsTextJobSetter{ + Content: omit.From(content), + Created: omit.From(time.Now()), + Destination: omit.From(destination), + //ID: + Type: omit.From(type_), + }).One(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to add delayed text job: %w", err) + } + log.Info().Int32("id", job.ID).Msg("Created delayed text job") + return nil +} + +func ensureInitialText(ctx context.Context, src string, dst string) error { + // + origin := enums.CommsTextoriginWebsiteAction + rows, err := models.CommsTextLogs.Query( + models.SelectWhere.CommsTextLogs.Destination.EQ(dst), + models.SelectWhere.CommsTextLogs.IsWelcome.EQ(true), + ).All(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to query text logs: %w", err) + } + if len(rows) > 0 { + return nil + } + content := "Welcome to Report Mosquitoes Online. We received your request and want to confirm text updates. Reply YES to continue. Reply STOP at any time to unsubscribe" + err = sendText(ctx, src, dst, content, origin, true) + if err != nil { + return fmt.Errorf("Failed to send initial confirmation: %w", err) + } + return nil +} + +func ensureInDB(ctx context.Context, destination string) (err error) { + _, err = models.FindCommsPhone(ctx, db.PGInstance.BobDB, destination) + if err != nil { + // doesn't exist + if err.Error() == "sql: no rows in result set" { + _, err = models.CommsPhones.Insert(&models.CommsPhoneSetter{ + E164: omit.From(destination), + IsSubscribed: omitnull.FromPtr[bool](nil), + }).One(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to insert new phone contact: %w", err) + } + log.Info().Str("phone", destination).Msg("Added text to the comms database") + return nil + } + return fmt.Errorf("Unexpected error searching for phone contact: %w", err) + } + return nil +} + +func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool) (err error) { + _, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ + //ID: + Content: omit.From(content), + Created: omit.From(time.Now()), + Destination: omit.From(destination), + IsWelcome: omit.From(is_welcome), + Origin: omit.From(origin), + Source: omit.From(source), + }).One(ctx, db.PGInstance.BobDB) + + return err +} + // Translate from Twilio's representation of a RCS message sender to our concept of a phone number // From: rcs:dev_report_mosquitoes_online_dosrvwxm_agent // To: +16235525879 @@ -50,6 +130,19 @@ func loadPreviousMessages(ctx context.Context, dst, src string) ([]llm.Message, return results, nil } +func sendText(ctx context.Context, source string, destination string, message string, origin enums.CommsTextorigin, is_welcome bool) error { + err := ensureInDB(ctx, destination) + if err != nil { + return fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err) + } + err = insertTextLog(ctx, message, destination, source, origin, is_welcome) + if err != nil { + return fmt.Errorf("Failed to insert text message in the DB: %w", err) + } + err = text.SendText(ctx, source, destination, message) + return nil +} + func splitPhoneSource(s string) (string, string) { parts := strings.Split(s, ":") switch len(parts) { @@ -117,15 +210,14 @@ func HandleTextMessage(from string, to string, body string) { setSubscribed(ctx, src, true) handleWaitingTextJobs(ctx, src) default: - err = text.SendInitialReprompt(ctx, dst, src) + content := "I have to start with either 'YES' or 'STOP' first, Which do you want?" + err := text.SendText(ctx, src, dst, content) if err != nil { log.Error().Err(err).Msg("Failed to resend initial prompt.") } } return } - if !(*subscribed) { - } previous_messages, err := loadPreviousMessages(ctx, dst, src) if err != nil { log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages") From b8e7b9b7fd0ad1e7398663b279545b7a573db511 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 27 Jan 2026 14:29:55 +0000 Subject: [PATCH 0145/1513] Working LLM responses and Twilio status tracking The responses aren't good, but they do exist. --- api/twilio.go | 1 + comms/text/text.go | 17 +- db/dberrors/comms.text_log.bob.go | 9 + db/dbinfo/comms.text_log.bob.go | 73 +++++- db/enums/enums.bob.go | 8 +- db/factory/bobfactory_main.bob.go | 2 + db/factory/comms.text_log.bob.go | 122 +++++++++- .../00044_comms_text_origin_user.sql | 6 + db/migrations/00045_comms_text_sid.sql | 8 + db/models/comms.text_log.bob.go | 148 ++++++++---- llm/client.go | 18 +- llm/openai.go | 162 ++++---------- platform/text.go | 210 +++++++++++------- 13 files changed, 497 insertions(+), 287 deletions(-) create mode 100644 db/migrations/00044_comms_text_origin_user.sql create mode 100644 db/migrations/00045_comms_text_sid.sql diff --git a/api/twilio.go b/api/twilio.go index 9a252e80..91ee54ba 100644 --- a/api/twilio.go +++ b/api/twilio.go @@ -18,6 +18,7 @@ func twilioStatusPost(w http.ResponseWriter, r *http.Request) { message_sid := r.PostFormValue("MessageSid") message_status := r.PostFormValue("MessageStatus") log.Info().Str("sid", message_sid).Str("status", message_status).Msg("Updated message status") + platform.UpdateMessageStatus(message_sid, message_status) fmt.Fprintf(w, "") } func twilioTextPost(w http.ResponseWriter, r *http.Request) { diff --git a/comms/text/text.go b/comms/text/text.go index 34d2aa7a..c98e33fc 100644 --- a/comms/text/text.go +++ b/comms/text/text.go @@ -18,7 +18,7 @@ func ParsePhoneNumber(input string) (*E164, error) { return phonenumbers.Parse(input, "US") } -func SendText(ctx context.Context, source string, destination string, message string) error { +func SendText(ctx context.Context, source string, destination string, message string) (string, error) { client := twilio.NewRestClient() params := &twilioApi.CreateMessageParams{} @@ -29,15 +29,14 @@ func SendText(ctx context.Context, source string, destination string, message st resp, err := client.Api.CreateMessage(params) if err != nil { - return fmt.Errorf("Failed to create message to %s: %w", destination, err) - } else { - if resp.Body != nil { - log.Info().Str("dest", destination).Str("body", *resp.Body).Msg("Text message response") - } else { - log.Info().Str("dest", destination).Msg("Text message response is nil") - } + return "", fmt.Errorf("Failed to create message to %s: %w", destination, err) } - return nil + //log.Info().Str("dest", destination).Str("sid", *resp.Body).Msg("Text message response") + if resp.Sid == nil { + log.Warn().Str("src", source).Str("dst", destination).Msg("Text message sid is nil") + return "", nil + } + return *resp.Sid, nil } func sendSMS(destination, source, message string) error { diff --git a/db/dberrors/comms.text_log.bob.go b/db/dberrors/comms.text_log.bob.go index 8bd6a9dc..541e7178 100644 --- a/db/dberrors/comms.text_log.bob.go +++ b/db/dberrors/comms.text_log.bob.go @@ -10,8 +10,17 @@ var CommsTextLogErrors = &commsTextLogErrors{ columns: []string{"id"}, s: "text_log_pkey", }, + + ErrUniqueTextLogTwilioSidKey: &UniqueConstraintError{ + schema: "comms", + table: "text_log", + columns: []string{"twilio_sid"}, + s: "text_log_twilio_sid_key", + }, } type commsTextLogErrors struct { ErrUniqueTextLogPkey *UniqueConstraintError + + ErrUniqueTextLogTwilioSidKey *UniqueConstraintError } diff --git a/db/dbinfo/comms.text_log.bob.go b/db/dbinfo/comms.text_log.bob.go index 0e4a1329..d85d75d6 100644 --- a/db/dbinfo/comms.text_log.bob.go +++ b/db/dbinfo/comms.text_log.bob.go @@ -78,6 +78,24 @@ var CommsTextLogs = Table[ Generated: false, AutoIncr: false, }, + TwilioSid: column{ + Name: "twilio_sid", + DBType: "text", + Default: "NULL", + Comment: "", + Nullable: true, + Generated: false, + AutoIncr: false, + }, + TwilioStatus: column{ + Name: "twilio_status", + DBType: "text", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, }, Indexes: commsTextLogIndexes{ TextLogPkey: index{ @@ -97,6 +115,23 @@ var CommsTextLogs = Table[ Where: "", Include: []string{}, }, + TextLogTwilioSidKey: index{ + Type: "btree", + Name: "text_log_twilio_sid_key", + Columns: []indexColumn{ + { + Name: "twilio_sid", + Desc: null.FromCond(false, true), + IsExpression: false, + }, + }, + Unique: true, + Comment: "", + NullsFirst: []bool{false}, + NullsDistinct: false, + Where: "", + Include: []string{}, + }, }, PrimaryKey: &constraint{ Name: "text_log_pkey", @@ -123,33 +158,43 @@ var CommsTextLogs = Table[ ForeignColumns: []string{"e164"}, }, }, + Uniques: commsTextLogUniques{ + TextLogTwilioSidKey: constraint{ + Name: "text_log_twilio_sid_key", + Columns: []string{"twilio_sid"}, + Comment: "", + }, + }, Comment: "Used to track text messages that were sent.", } type commsTextLogColumns struct { - Content column - Created column - Destination column - ID column - IsWelcome column - Origin column - Source column + Content column + Created column + Destination column + ID column + IsWelcome column + Origin column + Source column + TwilioSid column + TwilioStatus column } func (c commsTextLogColumns) AsSlice() []column { return []column{ - c.Content, c.Created, c.Destination, c.ID, c.IsWelcome, c.Origin, c.Source, + c.Content, c.Created, c.Destination, c.ID, c.IsWelcome, c.Origin, c.Source, c.TwilioSid, c.TwilioStatus, } } type commsTextLogIndexes struct { - TextLogPkey index + TextLogPkey index + TextLogTwilioSidKey index } func (i commsTextLogIndexes) AsSlice() []index { return []index{ - i.TextLogPkey, + i.TextLogPkey, i.TextLogTwilioSidKey, } } @@ -164,10 +209,14 @@ func (f commsTextLogForeignKeys) AsSlice() []foreignKey { } } -type commsTextLogUniques struct{} +type commsTextLogUniques struct { + TextLogTwilioSidKey constraint +} func (u commsTextLogUniques) AsSlice() []constraint { - return []constraint{} + return []constraint{ + u.TextLogTwilioSidKey, + } } type commsTextLogChecks struct{} diff --git a/db/enums/enums.bob.go b/db/enums/enums.bob.go index 81f030ec..cadb022f 100644 --- a/db/enums/enums.bob.go +++ b/db/enums/enums.bob.go @@ -347,6 +347,8 @@ const ( CommsTextoriginDistrict CommsTextorigin = "district" CommsTextoriginLLM CommsTextorigin = "llm" CommsTextoriginWebsiteAction CommsTextorigin = "website-action" + CommsTextoriginCustomer CommsTextorigin = "customer" + CommsTextoriginReiteration CommsTextorigin = "reiteration" ) func AllCommsTextorigin() []CommsTextorigin { @@ -354,6 +356,8 @@ func AllCommsTextorigin() []CommsTextorigin { CommsTextoriginDistrict, CommsTextoriginLLM, CommsTextoriginWebsiteAction, + CommsTextoriginCustomer, + CommsTextoriginReiteration, } } @@ -367,7 +371,9 @@ func (e CommsTextorigin) Valid() bool { switch e { case CommsTextoriginDistrict, CommsTextoriginLLM, - CommsTextoriginWebsiteAction: + CommsTextoriginWebsiteAction, + CommsTextoriginCustomer, + CommsTextoriginReiteration: return true default: return false diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index 45b2b33f..f8f5ca98 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -368,6 +368,8 @@ func (f *Factory) FromExistingCommsTextLog(m *models.CommsTextLog) *CommsTextLog o.IsWelcome = func() bool { return m.IsWelcome } o.Origin = func() enums.CommsTextorigin { return m.Origin } o.Source = func() string { return m.Source } + o.TwilioSid = func() null.Val[string] { return m.TwilioSid } + o.TwilioStatus = func() string { return m.TwilioStatus } ctx := context.Background() if m.R.DestinationPhone != nil { diff --git a/db/factory/comms.text_log.bob.go b/db/factory/comms.text_log.bob.go index d2e88519..5edb4b92 100644 --- a/db/factory/comms.text_log.bob.go +++ b/db/factory/comms.text_log.bob.go @@ -10,7 +10,9 @@ import ( enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" "github.com/stephenafamo/bob" ) @@ -36,13 +38,15 @@ func (mods CommsTextLogModSlice) Apply(ctx context.Context, n *CommsTextLogTempl // CommsTextLogTemplate is an object representing the database table. // all columns are optional and should be set by mods type CommsTextLogTemplate struct { - Content func() string - Created func() time.Time - Destination func() string - ID func() int32 - IsWelcome func() bool - Origin func() enums.CommsTextorigin - Source func() string + Content func() string + Created func() time.Time + Destination func() string + ID func() int32 + IsWelcome func() bool + Origin func() enums.CommsTextorigin + Source func() string + TwilioSid func() null.Val[string] + TwilioStatus func() string r commsTextLogR f *Factory @@ -120,6 +124,14 @@ func (o CommsTextLogTemplate) BuildSetter() *models.CommsTextLogSetter { val := o.Source() m.Source = omit.From(val) } + if o.TwilioSid != nil { + val := o.TwilioSid() + m.TwilioSid = omitnull.FromNull(val) + } + if o.TwilioStatus != nil { + val := o.TwilioStatus() + m.TwilioStatus = omit.From(val) + } return m } @@ -163,6 +175,12 @@ func (o CommsTextLogTemplate) Build() *models.CommsTextLog { if o.Source != nil { m.Source = o.Source() } + if o.TwilioSid != nil { + m.TwilioSid = o.TwilioSid() + } + if o.TwilioStatus != nil { + m.TwilioStatus = o.TwilioStatus() + } o.setModelRels(m) @@ -207,6 +225,10 @@ func ensureCreatableCommsTextLog(m *models.CommsTextLogSetter) { val := random_string(nil) m.Source = omit.From(val) } + if !(m.TwilioStatus.IsValue()) { + val := random_string(nil) + m.TwilioStatus = omit.From(val) + } } // insertOptRels creates and inserts any optional the relationships on *models.CommsTextLog @@ -351,6 +373,8 @@ func (m commsTextLogMods) RandomizeAllColumns(f *faker.Faker) CommsTextLogMod { CommsTextLogMods.RandomIsWelcome(f), CommsTextLogMods.RandomOrigin(f), CommsTextLogMods.RandomSource(f), + CommsTextLogMods.RandomTwilioSid(f), + CommsTextLogMods.RandomTwilioStatus(f), } } @@ -571,6 +595,90 @@ func (m commsTextLogMods) RandomSource(f *faker.Faker) CommsTextLogMod { }) } +// Set the model columns to this value +func (m commsTextLogMods) TwilioSid(val null.Val[string]) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.TwilioSid = func() null.Val[string] { return val } + }) +} + +// Set the Column from the function +func (m commsTextLogMods) TwilioSidFunc(f func() null.Val[string]) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.TwilioSid = f + }) +} + +// Clear any values for the column +func (m commsTextLogMods) UnsetTwilioSid() CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.TwilioSid = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +// The generated value is sometimes null +func (m commsTextLogMods) RandomTwilioSid(f *faker.Faker) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.TwilioSid = func() null.Val[string] { + if f == nil { + f = &defaultFaker + } + + val := random_string(f) + return null.From(val) + } + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +// The generated value is never null +func (m commsTextLogMods) RandomTwilioSidNotNull(f *faker.Faker) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.TwilioSid = func() null.Val[string] { + if f == nil { + f = &defaultFaker + } + + val := random_string(f) + return null.From(val) + } + }) +} + +// Set the model columns to this value +func (m commsTextLogMods) TwilioStatus(val string) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.TwilioStatus = func() string { return val } + }) +} + +// Set the Column from the function +func (m commsTextLogMods) TwilioStatusFunc(f func() string) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.TwilioStatus = f + }) +} + +// Clear any values for the column +func (m commsTextLogMods) UnsetTwilioStatus() CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.TwilioStatus = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextLogMods) RandomTwilioStatus(f *faker.Faker) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.TwilioStatus = func() string { + return random_string(f) + } + }) +} + func (m commsTextLogMods) WithParentsCascading() CommsTextLogMod { return CommsTextLogModFunc(func(ctx context.Context, o *CommsTextLogTemplate) { if isDone, _ := commsTextLogWithParentsCascadingCtx.Value(ctx); isDone { diff --git a/db/migrations/00044_comms_text_origin_user.sql b/db/migrations/00044_comms_text_origin_user.sql new file mode 100644 index 00000000..f8aa6829 --- /dev/null +++ b/db/migrations/00044_comms_text_origin_user.sql @@ -0,0 +1,6 @@ +-- +goose Up +ALTER TYPE comms.TextOrigin ADD VALUE 'customer'; +ALTER TYPE comms.TextOrigin ADD VALUE 'reiteration'; +-- +goose Down +ALTER TYPE comms.TextOrigin DROP VALUE 'reiteration'; +ALTER TYPE comms.TextOrigin DROP VALUE 'customer'; diff --git a/db/migrations/00045_comms_text_sid.sql b/db/migrations/00045_comms_text_sid.sql new file mode 100644 index 00000000..71b0e09e --- /dev/null +++ b/db/migrations/00045_comms_text_sid.sql @@ -0,0 +1,8 @@ +-- +goose Up +ALTER TABLE comms.text_log ADD COLUMN twilio_sid TEXT UNIQUE; +ALTER TABLE comms.text_log ADD COLUMN twilio_status TEXT; +UPDATE comms.text_log SET twilio_status = ''; +ALTER TABLE comms.text_log ALTER COLUMN twilio_status SET NOT NULL; +-- +goose Down +ALTER TABLE comms.text_log DROP COLUMN twilio_status; +ALTER TABLE comms.text_log DROP COLUMN twilio_sid; diff --git a/db/models/comms.text_log.bob.go b/db/models/comms.text_log.bob.go index 445ac37f..45e671d0 100644 --- a/db/models/comms.text_log.bob.go +++ b/db/models/comms.text_log.bob.go @@ -10,7 +10,9 @@ import ( "time" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" + "github.com/aarondl/opt/omitnull" "github.com/stephenafamo/bob" "github.com/stephenafamo/bob/dialect/psql" "github.com/stephenafamo/bob/dialect/psql/dialect" @@ -25,13 +27,15 @@ import ( // CommsTextLog is an object representing the database table. type CommsTextLog struct { - Content string `db:"content" ` - Created time.Time `db:"created" ` - Destination string `db:"destination" ` - ID int32 `db:"id,pk" ` - IsWelcome bool `db:"is_welcome" ` - Origin enums.CommsTextorigin `db:"origin" ` - Source string `db:"source" ` + Content string `db:"content" ` + Created time.Time `db:"created" ` + Destination string `db:"destination" ` + ID int32 `db:"id,pk" ` + IsWelcome bool `db:"is_welcome" ` + Origin enums.CommsTextorigin `db:"origin" ` + Source string `db:"source" ` + TwilioSid null.Val[string] `db:"twilio_sid" ` + TwilioStatus string `db:"twilio_status" ` R commsTextLogR `db:"-" ` } @@ -55,29 +59,33 @@ type commsTextLogR struct { func buildCommsTextLogColumns(alias string) commsTextLogColumns { return commsTextLogColumns{ ColumnsExpr: expr.NewColumnsExpr( - "content", "created", "destination", "id", "is_welcome", "origin", "source", + "content", "created", "destination", "id", "is_welcome", "origin", "source", "twilio_sid", "twilio_status", ).WithParent("comms.text_log"), - tableAlias: alias, - Content: psql.Quote(alias, "content"), - Created: psql.Quote(alias, "created"), - Destination: psql.Quote(alias, "destination"), - ID: psql.Quote(alias, "id"), - IsWelcome: psql.Quote(alias, "is_welcome"), - Origin: psql.Quote(alias, "origin"), - Source: psql.Quote(alias, "source"), + tableAlias: alias, + Content: psql.Quote(alias, "content"), + Created: psql.Quote(alias, "created"), + Destination: psql.Quote(alias, "destination"), + ID: psql.Quote(alias, "id"), + IsWelcome: psql.Quote(alias, "is_welcome"), + Origin: psql.Quote(alias, "origin"), + Source: psql.Quote(alias, "source"), + TwilioSid: psql.Quote(alias, "twilio_sid"), + TwilioStatus: psql.Quote(alias, "twilio_status"), } } type commsTextLogColumns struct { expr.ColumnsExpr - tableAlias string - Content psql.Expression - Created psql.Expression - Destination psql.Expression - ID psql.Expression - IsWelcome psql.Expression - Origin psql.Expression - Source psql.Expression + tableAlias string + Content psql.Expression + Created psql.Expression + Destination psql.Expression + ID psql.Expression + IsWelcome psql.Expression + Origin psql.Expression + Source psql.Expression + TwilioSid psql.Expression + TwilioStatus psql.Expression } func (c commsTextLogColumns) Alias() string { @@ -92,17 +100,19 @@ func (commsTextLogColumns) AliasedAs(alias string) commsTextLogColumns { // All values are optional, and do not have to be set // Generated columns are not included type CommsTextLogSetter struct { - Content omit.Val[string] `db:"content" ` - Created omit.Val[time.Time] `db:"created" ` - Destination omit.Val[string] `db:"destination" ` - ID omit.Val[int32] `db:"id,pk" ` - IsWelcome omit.Val[bool] `db:"is_welcome" ` - Origin omit.Val[enums.CommsTextorigin] `db:"origin" ` - Source omit.Val[string] `db:"source" ` + Content omit.Val[string] `db:"content" ` + Created omit.Val[time.Time] `db:"created" ` + Destination omit.Val[string] `db:"destination" ` + ID omit.Val[int32] `db:"id,pk" ` + IsWelcome omit.Val[bool] `db:"is_welcome" ` + Origin omit.Val[enums.CommsTextorigin] `db:"origin" ` + Source omit.Val[string] `db:"source" ` + TwilioSid omitnull.Val[string] `db:"twilio_sid" ` + TwilioStatus omit.Val[string] `db:"twilio_status" ` } func (s CommsTextLogSetter) SetColumns() []string { - vals := make([]string, 0, 7) + vals := make([]string, 0, 9) if s.Content.IsValue() { vals = append(vals, "content") } @@ -124,6 +134,12 @@ func (s CommsTextLogSetter) SetColumns() []string { if s.Source.IsValue() { vals = append(vals, "source") } + if !s.TwilioSid.IsUnset() { + vals = append(vals, "twilio_sid") + } + if s.TwilioStatus.IsValue() { + vals = append(vals, "twilio_status") + } return vals } @@ -149,6 +165,12 @@ func (s CommsTextLogSetter) Overwrite(t *CommsTextLog) { if s.Source.IsValue() { t.Source = s.Source.MustGet() } + if !s.TwilioSid.IsUnset() { + t.TwilioSid = s.TwilioSid.MustGetNull() + } + if s.TwilioStatus.IsValue() { + t.TwilioStatus = s.TwilioStatus.MustGet() + } } func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { @@ -157,7 +179,7 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { }) q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { - vals := make([]bob.Expression, 7) + vals := make([]bob.Expression, 9) if s.Content.IsValue() { vals[0] = psql.Arg(s.Content.MustGet()) } else { @@ -200,6 +222,18 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { vals[6] = psql.Raw("DEFAULT") } + if !s.TwilioSid.IsUnset() { + vals[7] = psql.Arg(s.TwilioSid.MustGetNull()) + } else { + vals[7] = psql.Raw("DEFAULT") + } + + if s.TwilioStatus.IsValue() { + vals[8] = psql.Arg(s.TwilioStatus.MustGet()) + } else { + vals[8] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -209,7 +243,7 @@ func (s CommsTextLogSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 7) + exprs := make([]bob.Expression, 0, 9) if s.Content.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -260,6 +294,20 @@ func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if !s.TwilioSid.IsUnset() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "twilio_sid")...), + psql.Arg(s.TwilioSid), + }}) + } + + if s.TwilioStatus.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "twilio_status")...), + psql.Arg(s.TwilioStatus), + }}) + } + return exprs } @@ -631,13 +679,15 @@ func (commsTextLog0 *CommsTextLog) AttachSourcePhone(ctx context.Context, exec b } type commsTextLogWhere[Q psql.Filterable] struct { - Content psql.WhereMod[Q, string] - Created psql.WhereMod[Q, time.Time] - Destination psql.WhereMod[Q, string] - ID psql.WhereMod[Q, int32] - IsWelcome psql.WhereMod[Q, bool] - Origin psql.WhereMod[Q, enums.CommsTextorigin] - Source psql.WhereMod[Q, string] + Content psql.WhereMod[Q, string] + Created psql.WhereMod[Q, time.Time] + Destination psql.WhereMod[Q, string] + ID psql.WhereMod[Q, int32] + IsWelcome psql.WhereMod[Q, bool] + Origin psql.WhereMod[Q, enums.CommsTextorigin] + Source psql.WhereMod[Q, string] + TwilioSid psql.WhereNullMod[Q, string] + TwilioStatus psql.WhereMod[Q, string] } func (commsTextLogWhere[Q]) AliasedAs(alias string) commsTextLogWhere[Q] { @@ -646,13 +696,15 @@ func (commsTextLogWhere[Q]) AliasedAs(alias string) commsTextLogWhere[Q] { func buildCommsTextLogWhere[Q psql.Filterable](cols commsTextLogColumns) commsTextLogWhere[Q] { return commsTextLogWhere[Q]{ - Content: psql.Where[Q, string](cols.Content), - Created: psql.Where[Q, time.Time](cols.Created), - Destination: psql.Where[Q, string](cols.Destination), - ID: psql.Where[Q, int32](cols.ID), - IsWelcome: psql.Where[Q, bool](cols.IsWelcome), - Origin: psql.Where[Q, enums.CommsTextorigin](cols.Origin), - Source: psql.Where[Q, string](cols.Source), + Content: psql.Where[Q, string](cols.Content), + Created: psql.Where[Q, time.Time](cols.Created), + Destination: psql.Where[Q, string](cols.Destination), + ID: psql.Where[Q, int32](cols.ID), + IsWelcome: psql.Where[Q, bool](cols.IsWelcome), + Origin: psql.Where[Q, enums.CommsTextorigin](cols.Origin), + Source: psql.Where[Q, string](cols.Source), + TwilioSid: psql.WhereNull[Q, string](cols.TwilioSid), + TwilioStatus: psql.Where[Q, string](cols.TwilioStatus), } } diff --git a/llm/client.go b/llm/client.go index 45ff2a24..af53d5f2 100644 --- a/llm/client.go +++ b/llm/client.go @@ -1,7 +1,9 @@ package llm import ( - "github.com/rs/zerolog/log" + "context" + "fmt" + //"github.com/rs/zerolog/log" ) type Message struct { @@ -9,14 +11,10 @@ type Message struct { IsFromCustomer bool } -func GenerateNextMessage(history []Message, current Message) (Message, error) { - // In general our history - for i, msg := range history { - log.Info().Int("i", i).Bool("is_customer", msg.IsFromCustomer).Msg("History") +func GenerateNextMessage(ctx context.Context, history []Message, customer_phone string) (Message, error) { + next, err := client.continueConversation(ctx, history, customer_phone) + if err != nil { + return Message{}, fmt.Errorf("Failed to generate next message: %w", err) } - - return Message{ - Content: "hey there. :)", - IsFromCustomer: false, - }, nil + return next, nil } diff --git a/llm/openai.go b/llm/openai.go index d936bb0d..b971d1a3 100644 --- a/llm/openai.go +++ b/llm/openai.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "strings" - "time" "github.com/maruel/genai" "github.com/maruel/genai/adapters" @@ -12,21 +11,6 @@ import ( "github.com/rs/zerolog/log" ) -type openAIClient struct { - client *openaichat.Client - conversations map[string][]genai.Message - log *Logger -} - -var client *openAIClient - -type AIRequest struct { - Displayname string - Message string - Sender string - Timestamp time.Time -} - func CreateOpenAIClient(ctx context.Context) error { logger := createLogger() @@ -45,131 +29,73 @@ func CreateOpenAIClient(ctx context.Context) error { return nil } -func (c *openAIClient) continueConversation(ctx context.Context, req AIRequest) error { - msgs, ok := c.conversations["roomid"] - if !ok { - msgs = genai.Messages{ - c.startConversation(ctx, req), - } - } else { - msgs = append(msgs, genai.NewTextMessage(fmt.Sprintf("(%s) user: %s\nbot: ", req.Timestamp.String(), req.Message))) - } +type openAIClient struct { + client *openaichat.Client + conversations map[string][]genai.Message + log *Logger +} - c.log.Debug().Msg("Generating response...") +type QueryReportStatusInput struct { + ReportID string `json:"report_id"` +} + +var client *openAIClient + +func (c *openAIClient) continueConversation(ctx context.Context, history []Message, customer_phone string) (Message, error) { opts := genai.OptionsTools{ Tools: []genai.ToolDef{ { - Name: "followup_timer", - Description: "This should be used to indicate that the bot should follow up with the user in the future to check on task progress.", - Callback: func(ctx2 context.Context, input *FollowupTimerInput) (string, error) { - return c.followupSchedule(ctx2, req, input) - }, - }, { - Name: "switch_task", - Description: "Any time the user indicates they change tasks this must be called to update the record of what tasks are being done.", - Callback: func(ctx2 context.Context, input *SwitchTaskInput) (string, error) { - return c.switchTask(ctx2, req, input) + Name: "query_report_status", + Description: "This is used to answer any questions about the current state of the mosquito nuisance report.", + Callback: func(ctx2 context.Context, input *QueryReportStatusInput) (string, error) { + return c.queryReportStatus(ctx2, customer_phone) }, }, }, } - res, _, err := adapters.GenSyncWithToolCallLoop(ctx, c.client, msgs, &opts) + msg := c.convertHistory(history) + res, _, err := adapters.GenSyncWithToolCallLoop(ctx, c.client, genai.Messages{msg}, &opts) if err != nil { - return fmt.Errorf("Failed to continue conversation: %v", err) + return Message{}, fmt.Errorf("Failed to continue conversation: %v", err) } for _, m := range res { - msgs = append(msgs, m) // Empty responses are tool call related. if m.String() == "" { + log.Debug().Msg("Tool called") } else { - //c.log.Info().Str("room", req.RoomID.String()).Msg(m.String()) var toSay string = m.String() - toSay = strings.Replace(toSay, "bot: ", "", 1) - log.Info().Str("to say", toSay).Msg("Responding") - /*c.aiResponseChannel <- AIResponse{ - Message: toSay, - RoomID: req.RoomID, - }*/ + toSay = strings.Replace(toSay, "report-mosquitoes-online: ", "", 1) + return Message{ + Content: toSay, + IsFromCustomer: false, + }, nil } } - //c.conversations[req.RoomID.String()] = msgs - return nil + return Message{}, nil } -type FollowupTimerInput struct { - DelayInSeconds int64 `json:"delay_in_seconds"` -} - -func (c *openAIClient) followupFire(ctx context.Context, req AIRequest, duration time.Duration) { - if err := ctx.Err(); err != nil { - //c.log.Info().Str("room", req.RoomID.String()).Msg("Context canceled") - return +func (c *openAIClient) convertHistory(history []Message) genai.Message { + var sb strings.Builder + sb.WriteString( + `This is a text chat conversation between a customer that's a member of the public and a mosquito abatement district. + The customer has reported a mosquito nuisance or mosquito breeding through the website report.mosquitoes.online. + Messages from the customer are prefixed with 'customer:' and reponses from the service agent servicing the request are prefixed with 'agent:'. + The agent wants to provide clear, confident, and succint information about the state of the customer's request. The agent also provides general information about how members of the public can help with controlling mosquitoes. For complex or highly specific requests, the agent will need to defer to the mosquito abatement district. This will take some time because contacting the district may take a few hours to get a response. When the agent needs to contact the district, the agent should tell the customer they are reaching out to the district and to expect a delay. + Transcript starts:`, + ) + for _, h := range history { + if h.IsFromCustomer { + sb.WriteString(fmt.Sprintf("\n\ncustomer (%s): %s\n", h.Content)) + } else { + sb.WriteString(fmt.Sprintf("\n\nagent (%s): %s\n", h.Content)) + } } - msgs, ok := c.conversations["roomid"] - if !ok { - //c.log.Warn().Str("room", req.RoomID.String()).Str("elapsed", duration.String()).Msg("No messages for room") - return - } - msgs = append(msgs, genai.NewTextMessage(fmt.Sprintf("<%s passed>", duration.String()))) - res, err := c.client.GenSync(ctx, msgs) - if err != nil { - //c.log.Error().Str("room", req.RoomID.String()).Err(err).Msg("Failed to continue after timer") - return - } - msgs = append(msgs, res.Message) - var toSay string = res.String() - toSay = strings.Replace(toSay, "bot: ", "", 1) - log.Info().Str("to say", toSay).Msg("To say") - /*c.aiResponseChannel <- AIResponse{ - Message: toSay, - RoomID: req.RoomID, - } - c.conversations[req.RoomID.String()] = msgs - */ + return genai.NewTextMessage(sb.String()) } -func (c *openAIClient) followupSchedule(ctx context.Context, req AIRequest, input *FollowupTimerInput) (string, error) { - //c.log.Info().Str("room", req.RoomID.String()).Int64("delay", input.DelayInSeconds).Msg("Followup timer scheduled.") - duration, err := time.ParseDuration(fmt.Sprintf("%ds", input.DelayInSeconds)) - if err != nil { - return "", fmt.Errorf("Failed to parse %d as a valid duration: %v", input.DelayInSeconds, err) - } - /*c.aiResponseChannel <- AIResponse{ - Message: fmt.Sprintf("⌛ followup scheduled '%s'", duration.String()), - RoomID: req.RoomID, - }*/ - time.AfterFunc(duration, func() { - c.followupFire(ctx, req, duration) - }) - return fmt.Sprintf("Followup timer set for %s in the future", duration.String()), nil -} - -type SwitchTaskInput struct { - TaskName string `json:"task_name"` -} - -func (c *openAIClient) switchTask(ctx context.Context, req AIRequest, input *SwitchTaskInput) (string, error) { - //c.log.Info().Str("room", req.RoomID.String()).Str("task", input.TaskName).Msg("Task Switched") - /*c.aiResponseChannel <- AIResponse{ - Message: fmt.Sprintf("📋 notes task '%s'", input.TaskName), - RoomID: req.RoomID, - }*/ - - return fmt.Sprintf("Recorded a switch to task %s at %s", input.TaskName, time.Now().String()), nil -} - -func (c *openAIClient) startConversation(ctx context.Context, req AIRequest) genai.Message { - return genai.NewTextMessage(fmt.Sprintf( - `This is a text chat conversation between an employee and a chatbot helping to manage timecards. - The user's name is '%[1]s'. - Messages from the user will start with '(timestamp) %[1]s:'. - Messages from the bot will start with 'bot:'. - Sometimes the user won't say anything for a long time and the chatbot needs to follow-up with them. - When time passes, there will be a prompt like '<200s passed>'. - The bot should then prompt the user to provide a bit of information about what they've been working on during that time. - The bot should be interested to know what the user's goals are at a high level and should pay attention to any difficulties or frustrations the user experiences.\n\n - (%[2]s) user: %[3]s\nbot:`, req.Displayname, req.Timestamp.String(), req.Message)) +func (c *openAIClient) queryReportStatus(ctx context.Context, customer_phone string) (string, error) { + return "Report is scheduled for work in 3 days at 2:00pm by the district", nil } diff --git a/platform/text.go b/platform/text.go index f77a6e77..cebc16e4 100644 --- a/platform/text.go +++ b/platform/text.go @@ -19,12 +19,97 @@ import ( "github.com/rs/zerolog/log" ) +func HandleTextMessage(from string, to string, body string) { + ctx := context.Background() + type_, src := splitPhoneSource(from) + dst, err := getDst(ctx, to) + if err != nil { + log.Error().Err(err).Str("to", to).Msg("Failed to get dst") + return + } + + _, err = insertTextLog(ctx, body, dst, src, enums.CommsTextoriginCustomer, false) + if err != nil { + log.Error().Err(err).Str("dst", dst).Msg("Failed to add text message log") + return + } + subscribed, err := isSubscribed(ctx, src) + if err != nil { + log.Error().Err(err).Msg("Failed to handle message") + return + } + // We don't know if they're subscribed or not. + if subscribed == nil { + body_l := strings.TrimSpace(strings.ToLower(body)) + switch body_l { + case "stop": + setSubscribed(ctx, src, false) + case "yes": + setSubscribed(ctx, src, true) + handleWaitingTextJobs(ctx, src) + default: + content := "I have to start with either 'YES' or 'STOP' first, Which do you want?" + /*err := insertTextLog(ctx, body, src, dst, enums.CommsTextoriginReiteration, false) + if err != nil { + log.Error().Err(err).Msg("Failed to add reiteration to the text log") + return + }*/ + err = sendText(ctx, src, dst, content, enums.CommsTextoriginReiteration, false) + if err != nil { + log.Error().Err(err).Msg("Failed to resend initial prompt.") + } + } + return + } + previous_messages, err := loadPreviousMessages(ctx, dst, src) + if err != nil { + log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages") + return + } + log.Info().Int("len", len(previous_messages)).Msg("passing") + next_message, err := llm.GenerateNextMessage(ctx, previous_messages, src) + if err != nil { + log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to generate next message") + return + } + /* + err = insertTextLog(ctx, next_message.Content, src, dst, enums.CommsTextoriginLLM, false) + if err != nil { + log.Error().Err(err).Str("dst", dst).Msg("Failed to insert new text message to the text log") + return + } + */ + err = sendText(ctx, dst, src, next_message.Content, enums.CommsTextoriginLLM, false) + if err != nil { + log.Error().Err(err).Str("src", src).Str("dst", dst).Str("content", next_message.Content).Msg("Failed to send response text") + return + } + log.Info().Str("from", from).Str("from-type", type_).Str("to", to).Str("src", src).Str("dst", dst).Str("body", body).Str("reply", next_message.Content).Msg("Handled text message") +} + func TextStoreSources() error { ctx := context.TODO() src := phonenumbers.Format(&config.PhoneNumberReport, phonenumbers.E164) return ensureInDB(ctx, src) } +func UpdateMessageStatus(twilio_sid string, status string) { + ctx := context.TODO() + l, err := models.CommsTextLogs.Query( + models.SelectWhere.CommsTextLogs.TwilioSid.EQ(twilio_sid), + ).One(ctx, db.PGInstance.BobDB) + if err != nil { + log.Error().Err(err).Str("twilio_sid", twilio_sid).Str("status", status).Msg("Failed to update message status query failed") + return + } + err = l.Update(ctx, db.PGInstance.BobDB, &models.CommsTextLogSetter{ + TwilioStatus: omit.From(status), + }) + if err != nil { + log.Error().Err(err).Str("twilio_sid", twilio_sid).Str("status", status).Msg("Failed to update message status update failed") + return + } +} func delayMessage(ctx context.Context, source string, destination string, content string, type_ enums.CommsTextjobtype) error { job, err := models.CommsTextJobs.Insert(&models.CommsTextJobSetter{ Content: omit.From(content), @@ -81,20 +166,6 @@ func ensureInDB(ctx context.Context, destination string) (err error) { return nil } -func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool) (err error) { - _, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ - //ID: - Content: omit.From(content), - Created: omit.From(time.Now()), - Destination: omit.From(destination), - IsWelcome: omit.From(is_welcome), - Origin: omit.From(origin), - Source: omit.From(source), - }).One(ctx, db.PGInstance.BobDB) - - return err -} - // Translate from Twilio's representation of a RCS message sender to our concept of a phone number // From: rcs:dev_report_mosquitoes_online_dosrvwxm_agent // To: +16235525879 @@ -113,6 +184,39 @@ func getDst(ctx context.Context, to string) (string, error) { return "", fmt.Errorf("Cannot match phone number to '%s'", to) } +func handleWaitingTextJobs(ctx context.Context, src string) { + log.Info().Str("src", src).Msg("Pretend handle waiting jobs") + +} + +func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool) (log *models.CommsTextLog, err error) { + log, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ + //ID: + Content: omit.From(content), + Created: omit.From(time.Now()), + Destination: omit.From(destination), + IsWelcome: omit.From(is_welcome), + Origin: omit.From(origin), + Source: omit.From(source), + TwilioSid: omitnull.FromPtr[string](nil), + TwilioStatus: omit.From(""), + }).One(ctx, db.PGInstance.BobDB) + + return log, err +} + +func isSubscribed(ctx context.Context, src string) (*bool, error) { + phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src) + if err != nil { + return nil, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) + } + if phone.IsSubscribed.IsNull() { + return nil, nil + } + result := phone.IsSubscribed.MustGet() + return &result, nil +} + func loadPreviousMessages(ctx context.Context, dst, src string) ([]llm.Message, error) { messages, err := sql.TextsBySenders(dst, src).All(ctx, db.PGInstance.BobDB) results := make([]llm.Message, 0) @@ -135,11 +239,19 @@ func sendText(ctx context.Context, source string, destination string, message st if err != nil { return fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err) } - err = insertTextLog(ctx, message, destination, source, origin, is_welcome) + log, err := insertTextLog(ctx, message, destination, source, origin, is_welcome) if err != nil { return fmt.Errorf("Failed to insert text message in the DB: %w", err) } - err = text.SendText(ctx, source, destination, message) + sid, err := text.SendText(ctx, source, destination, message) + if err != nil { + return fmt.Errorf("Failed to send text message: %w", err) + } + err = log.Update(ctx, db.PGInstance.BobDB, &models.CommsTextLogSetter{ + TwilioSid: omitnull.From(sid), + TwilioStatus: omit.From("created"), + }) + return nil } @@ -159,18 +271,6 @@ func splitPhoneSource(s string) (string, string) { } -func isSubscribed(ctx context.Context, src string) (*bool, error) { - phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src) - if err != nil { - return nil, fmt.Errorf("Failed to determine if '%s' is subscribed: %w", src, err) - } - if phone.IsSubscribed.IsNull() { - return nil, nil - } - result := phone.IsSubscribed.MustGet() - return &result, nil -} - func setSubscribed(ctx context.Context, src string, is_subscribed bool) error { phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, src) if err != nil { @@ -182,57 +282,3 @@ func setSubscribed(ctx context.Context, src string, is_subscribed bool) error { log.Info().Str("src", src).Bool("is_subscribed", is_subscribed).Msg("Set number subscribed") return nil } - -func handleWaitingTextJobs(ctx context.Context, src string) { - log.Info().Str("src", src).Msg("Pretend handle waiting jobs") - -} -func HandleTextMessage(from string, to string, body string) { - ctx := context.Background() - type_, src := splitPhoneSource(from) - dst, err := getDst(ctx, to) - if err != nil { - log.Error().Err(err).Str("to", to).Msg("Failed to get dst") - return - } - subscribed, err := isSubscribed(ctx, src) - if err != nil { - log.Error().Err(err).Msg("Failed to handle message") - return - } - // We don't know if they're subscribed or not. - if subscribed == nil { - body_l := strings.TrimSpace(strings.ToLower(body)) - switch body_l { - case "stop": - setSubscribed(ctx, src, false) - case "yes": - setSubscribed(ctx, src, true) - handleWaitingTextJobs(ctx, src) - default: - content := "I have to start with either 'YES' or 'STOP' first, Which do you want?" - err := text.SendText(ctx, src, dst, content) - if err != nil { - log.Error().Err(err).Msg("Failed to resend initial prompt.") - } - } - return - } - previous_messages, err := loadPreviousMessages(ctx, dst, src) - if err != nil { - log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages") - return - } - current := llm.Message{ - Content: body, - IsFromCustomer: true, - } - log.Info().Int("len", len(previous_messages)).Msg("passing") - next_message, err := llm.GenerateNextMessage(previous_messages, current) - if err != nil { - log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to generate next message") - return - } - text.SendTextFromLLM(next_message.Content) - log.Info().Str("from", from).Str("from-type", type_).Str("to", to).Str("src", src).Str("dst", dst).Str("body", body).Str("reply", next_message.Content).Msg("Handling text message") -} From a68b8781e785de412418eaec0434ac2538b1c5e3 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 27 Jan 2026 18:44:02 +0000 Subject: [PATCH 0146/1513] Add ability to make LLM agent forget the conversation history This is extremely useful for testing. In order to do this I needed to actually deploy the migration to a bob fork so I could start to add support for behaviors I really want. Specifically the ability to search for ids in a slice. --- auth/auth.go | 4 +- background/arcgis.go | 8 +- background/summary.go | 10 +- comms/email/db.go | 2 +- comms/email/template.go | 6 +- comms/text/db.go | 2 +- db/connection.go | 2 +- db/dberrors/arcgis.user_.bob.go | 2 +- db/dberrors/arcgis.user_privilege.bob.go | 2 +- db/dberrors/bob_errors.bob.go | 2 +- db/dberrors/comms.email_contact.bob.go | 2 +- db/dberrors/comms.email_log.bob.go | 2 +- db/dberrors/comms.email_template.bob.go | 2 +- db/dberrors/comms.phone.bob.go | 2 +- db/dberrors/comms.text_job.bob.go | 2 +- db/dberrors/comms.text_log.bob.go | 2 +- .../fieldseeker.containerrelate.bob.go | 2 +- .../fieldseeker.fieldscoutinglog.bob.go | 2 +- db/dberrors/fieldseeker.habitatrelate.bob.go | 2 +- .../fieldseeker.inspectionsample.bob.go | 2 +- .../fieldseeker.inspectionsampledetail.bob.go | 2 +- db/dberrors/fieldseeker.linelocation.bob.go | 2 +- .../fieldseeker.locationtracking.bob.go | 2 +- .../fieldseeker.mosquitoinspection.bob.go | 2 +- db/dberrors/fieldseeker.pointlocation.bob.go | 2 +- .../fieldseeker.polygonlocation.bob.go | 2 +- db/dberrors/fieldseeker.pool.bob.go | 2 +- db/dberrors/fieldseeker.pooldetail.bob.go | 2 +- .../fieldseeker.proposedtreatmentarea.bob.go | 2 +- .../fieldseeker.qamosquitoinspection.bob.go | 2 +- db/dberrors/fieldseeker.rodentlocation.bob.go | 2 +- .../fieldseeker.samplecollection.bob.go | 2 +- db/dberrors/fieldseeker.samplelocation.bob.go | 2 +- db/dberrors/fieldseeker.servicerequest.bob.go | 2 +- .../fieldseeker.speciesabundance.bob.go | 2 +- db/dberrors/fieldseeker.stormdrain.bob.go | 2 +- db/dberrors/fieldseeker.timecard.bob.go | 2 +- db/dberrors/fieldseeker.trapdata.bob.go | 2 +- db/dberrors/fieldseeker.traplocation.bob.go | 2 +- db/dberrors/fieldseeker.treatment.bob.go | 2 +- db/dberrors/fieldseeker.treatmentarea.bob.go | 2 +- db/dberrors/fieldseeker.zones.bob.go | 2 +- db/dberrors/fieldseeker.zones2.bob.go | 2 +- db/dberrors/fieldseeker_sync.bob.go | 2 +- db/dberrors/goose_db_version.bob.go | 2 +- db/dberrors/h3_aggregation.bob.go | 2 +- db/dberrors/import.district.bob.go | 2 +- db/dberrors/note_audio.bob.go | 2 +- db/dberrors/note_audio_breadcrumb.bob.go | 2 +- db/dberrors/note_audio_data.bob.go | 2 +- db/dberrors/note_image.bob.go | 2 +- db/dberrors/note_image_breadcrumb.bob.go | 2 +- db/dberrors/note_image_data.bob.go | 2 +- db/dberrors/notification.bob.go | 2 +- db/dberrors/oauth_token.bob.go | 2 +- db/dberrors/organization.bob.go | 2 +- db/dberrors/publicreport.image.bob.go | 2 +- db/dberrors/publicreport.image_exif.bob.go | 2 +- db/dberrors/publicreport.nuisance.bob.go | 2 +- db/dberrors/publicreport.pool.bob.go | 2 +- db/dberrors/publicreport.pool_image.bob.go | 2 +- db/dberrors/publicreport.quick.bob.go | 2 +- db/dberrors/publicreport.quick_image.bob.go | 2 +- db/dberrors/sessions.bob.go | 2 +- db/dberrors/spatial_ref_sys.bob.go | 2 +- db/dberrors/user_.bob.go | 2 +- db/dbinfo/arcgis.user_.bob.go | 2 +- db/dbinfo/arcgis.user_privilege.bob.go | 2 +- db/dbinfo/bob_types.bob.go | 2 +- db/dbinfo/comms.email_contact.bob.go | 2 +- db/dbinfo/comms.email_log.bob.go | 2 +- db/dbinfo/comms.email_template.bob.go | 2 +- db/dbinfo/comms.phone.bob.go | 2 +- db/dbinfo/comms.text_job.bob.go | 2 +- db/dbinfo/comms.text_log.bob.go | 32 ++-- db/dbinfo/fieldseeker.containerrelate.bob.go | 2 +- db/dbinfo/fieldseeker.fieldscoutinglog.bob.go | 2 +- db/dbinfo/fieldseeker.habitatrelate.bob.go | 2 +- db/dbinfo/fieldseeker.inspectionsample.bob.go | 2 +- .../fieldseeker.inspectionsampledetail.bob.go | 2 +- db/dbinfo/fieldseeker.linelocation.bob.go | 2 +- db/dbinfo/fieldseeker.locationtracking.bob.go | 2 +- .../fieldseeker.mosquitoinspection.bob.go | 2 +- db/dbinfo/fieldseeker.pointlocation.bob.go | 2 +- db/dbinfo/fieldseeker.polygonlocation.bob.go | 2 +- db/dbinfo/fieldseeker.pool.bob.go | 2 +- db/dbinfo/fieldseeker.pooldetail.bob.go | 2 +- .../fieldseeker.proposedtreatmentarea.bob.go | 2 +- .../fieldseeker.qamosquitoinspection.bob.go | 2 +- db/dbinfo/fieldseeker.rodentlocation.bob.go | 2 +- db/dbinfo/fieldseeker.samplecollection.bob.go | 2 +- db/dbinfo/fieldseeker.samplelocation.bob.go | 2 +- db/dbinfo/fieldseeker.servicerequest.bob.go | 2 +- db/dbinfo/fieldseeker.speciesabundance.bob.go | 2 +- db/dbinfo/fieldseeker.stormdrain.bob.go | 2 +- db/dbinfo/fieldseeker.timecard.bob.go | 2 +- db/dbinfo/fieldseeker.trapdata.bob.go | 2 +- db/dbinfo/fieldseeker.traplocation.bob.go | 2 +- db/dbinfo/fieldseeker.treatment.bob.go | 2 +- db/dbinfo/fieldseeker.treatmentarea.bob.go | 2 +- db/dbinfo/fieldseeker.zones.bob.go | 2 +- db/dbinfo/fieldseeker.zones2.bob.go | 2 +- db/dbinfo/fieldseeker_sync.bob.go | 2 +- db/dbinfo/geography_columns.bob.go | 2 +- db/dbinfo/geometry_columns.bob.go | 2 +- db/dbinfo/goose_db_version.bob.go | 2 +- db/dbinfo/h3_aggregation.bob.go | 2 +- db/dbinfo/import.district.bob.go | 2 +- db/dbinfo/note_audio.bob.go | 2 +- db/dbinfo/note_audio_breadcrumb.bob.go | 2 +- db/dbinfo/note_audio_data.bob.go | 2 +- db/dbinfo/note_image.bob.go | 2 +- db/dbinfo/note_image_breadcrumb.bob.go | 2 +- db/dbinfo/note_image_data.bob.go | 2 +- db/dbinfo/notification.bob.go | 2 +- db/dbinfo/oauth_token.bob.go | 2 +- db/dbinfo/organization.bob.go | 2 +- db/dbinfo/publicreport.image.bob.go | 2 +- db/dbinfo/publicreport.image_exif.bob.go | 2 +- db/dbinfo/publicreport.nuisance.bob.go | 2 +- db/dbinfo/publicreport.pool.bob.go | 2 +- db/dbinfo/publicreport.pool_image.bob.go | 2 +- db/dbinfo/publicreport.quick.bob.go | 2 +- db/dbinfo/publicreport.quick_image.bob.go | 2 +- db/dbinfo/publicreport.report_location.bob.go | 2 +- db/dbinfo/raster_columns.bob.go | 2 +- db/dbinfo/raster_overviews.bob.go | 2 +- db/dbinfo/sessions.bob.go | 2 +- db/dbinfo/spatial_ref_sys.bob.go | 2 +- db/dbinfo/user_.bob.go | 2 +- db/enums/enums.bob.go | 17 +- db/factory/arcgis.user_.bob.go | 4 +- db/factory/arcgis.user_privilege.bob.go | 4 +- db/factory/bobfactory_context.bob.go | 2 +- db/factory/bobfactory_main.bob.go | 7 +- db/factory/bobfactory_random.bob.go | 6 +- db/factory/comms.email_contact.bob.go | 4 +- db/factory/comms.email_log.bob.go | 6 +- db/factory/comms.email_template.bob.go | 4 +- db/factory/comms.phone.bob.go | 4 +- db/factory/comms.text_job.bob.go | 4 +- db/factory/comms.text_log.bob.go | 66 +++++-- db/factory/fieldseeker.containerrelate.bob.go | 6 +- .../fieldseeker.fieldscoutinglog.bob.go | 6 +- db/factory/fieldseeker.habitatrelate.bob.go | 6 +- .../fieldseeker.inspectionsample.bob.go | 6 +- .../fieldseeker.inspectionsampledetail.bob.go | 6 +- db/factory/fieldseeker.linelocation.bob.go | 6 +- .../fieldseeker.locationtracking.bob.go | 6 +- .../fieldseeker.mosquitoinspection.bob.go | 6 +- db/factory/fieldseeker.pointlocation.bob.go | 6 +- db/factory/fieldseeker.polygonlocation.bob.go | 6 +- db/factory/fieldseeker.pool.bob.go | 6 +- db/factory/fieldseeker.pooldetail.bob.go | 6 +- .../fieldseeker.proposedtreatmentarea.bob.go | 6 +- .../fieldseeker.qamosquitoinspection.bob.go | 6 +- db/factory/fieldseeker.rodentlocation.bob.go | 6 +- .../fieldseeker.samplecollection.bob.go | 6 +- db/factory/fieldseeker.samplelocation.bob.go | 6 +- db/factory/fieldseeker.servicerequest.bob.go | 6 +- .../fieldseeker.speciesabundance.bob.go | 6 +- db/factory/fieldseeker.stormdrain.bob.go | 6 +- db/factory/fieldseeker.timecard.bob.go | 6 +- db/factory/fieldseeker.trapdata.bob.go | 6 +- db/factory/fieldseeker.traplocation.bob.go | 6 +- db/factory/fieldseeker.treatment.bob.go | 6 +- db/factory/fieldseeker.treatmentarea.bob.go | 6 +- db/factory/fieldseeker.zones.bob.go | 6 +- db/factory/fieldseeker.zones2.bob.go | 6 +- db/factory/fieldseeker_sync.bob.go | 4 +- db/factory/geography_columns.bob.go | 2 +- db/factory/geometry_columns.bob.go | 2 +- db/factory/goose_db_version.bob.go | 4 +- db/factory/h3_aggregation.bob.go | 4 +- db/factory/import.district.bob.go | 4 +- db/factory/note_audio.bob.go | 4 +- db/factory/note_audio_breadcrumb.bob.go | 4 +- db/factory/note_audio_data.bob.go | 4 +- db/factory/note_image.bob.go | 4 +- db/factory/note_image_breadcrumb.bob.go | 4 +- db/factory/note_image_data.bob.go | 4 +- db/factory/notification.bob.go | 4 +- db/factory/oauth_token.bob.go | 4 +- db/factory/organization.bob.go | 4 +- db/factory/publicreport.image.bob.go | 4 +- db/factory/publicreport.image_exif.bob.go | 4 +- db/factory/publicreport.nuisance.bob.go | 4 +- db/factory/publicreport.pool.bob.go | 4 +- db/factory/publicreport.pool_image.bob.go | 4 +- db/factory/publicreport.quick.bob.go | 4 +- db/factory/publicreport.quick_image.bob.go | 4 +- .../publicreport.report_location.bob.go | 2 +- db/factory/raster_columns.bob.go | 2 +- db/factory/raster_overviews.bob.go | 2 +- db/factory/sessions.bob.go | 4 +- db/factory/spatial_ref_sys.bob.go | 4 +- db/factory/user_.bob.go | 4 +- db/fieldseeker.go | 4 +- .../00044_comms_text_origin_user.sql | 3 - db/migrations/00045_comms_text_sid.sql | 5 + db/models/arcgis.user_.bob.go | 22 +-- db/models/arcgis.user_privilege.bob.go | 22 +-- db/models/bob_counts.bob.go | 10 +- db/models/bob_joins.bob.go | 8 +- db/models/bob_loaders.bob.go | 8 +- db/models/bob_where.bob.go | 8 +- db/models/comms.email_contact.bob.go | 22 +-- db/models/comms.email_log.bob.go | 22 +-- db/models/comms.email_template.bob.go | 22 +-- db/models/comms.phone.bob.go | 22 +-- db/models/comms.text_job.bob.go | 22 +-- db/models/comms.text_log.bob.go | 167 ++++++++++-------- db/models/fieldseeker.containerrelate.bob.go | 24 +-- db/models/fieldseeker.fieldscoutinglog.bob.go | 24 +-- db/models/fieldseeker.habitatrelate.bob.go | 24 +-- db/models/fieldseeker.inspectionsample.bob.go | 24 +-- .../fieldseeker.inspectionsampledetail.bob.go | 24 +-- db/models/fieldseeker.linelocation.bob.go | 24 +-- db/models/fieldseeker.locationtracking.bob.go | 24 +-- .../fieldseeker.mosquitoinspection.bob.go | 24 +-- db/models/fieldseeker.pointlocation.bob.go | 24 +-- db/models/fieldseeker.polygonlocation.bob.go | 24 +-- db/models/fieldseeker.pool.bob.go | 24 +-- db/models/fieldseeker.pooldetail.bob.go | 24 +-- .../fieldseeker.proposedtreatmentarea.bob.go | 24 +-- .../fieldseeker.qamosquitoinspection.bob.go | 24 +-- db/models/fieldseeker.rodentlocation.bob.go | 24 +-- db/models/fieldseeker.samplecollection.bob.go | 24 +-- db/models/fieldseeker.samplelocation.bob.go | 24 +-- db/models/fieldseeker.servicerequest.bob.go | 24 +-- db/models/fieldseeker.speciesabundance.bob.go | 24 +-- db/models/fieldseeker.stormdrain.bob.go | 24 +-- db/models/fieldseeker.timecard.bob.go | 24 +-- db/models/fieldseeker.trapdata.bob.go | 24 +-- db/models/fieldseeker.traplocation.bob.go | 24 +-- db/models/fieldseeker.treatment.bob.go | 24 +-- db/models/fieldseeker.treatmentarea.bob.go | 24 +-- db/models/fieldseeker.zones.bob.go | 24 +-- db/models/fieldseeker.zones2.bob.go | 24 +-- db/models/fieldseeker_sync.bob.go | 22 +-- db/models/geography_columns.bob.go | 8 +- db/models/geometry_columns.bob.go | 8 +- db/models/goose_db_version.bob.go | 16 +- db/models/h3_aggregation.bob.go | 22 +-- db/models/import.district.bob.go | 22 +-- db/models/note_audio.bob.go | 22 +-- db/models/note_audio_breadcrumb.bob.go | 22 +-- db/models/note_audio_data.bob.go | 22 +-- db/models/note_image.bob.go | 22 +-- db/models/note_image_breadcrumb.bob.go | 22 +-- db/models/note_image_data.bob.go | 22 +-- db/models/notification.bob.go | 22 +-- db/models/oauth_token.bob.go | 22 +-- db/models/organization.bob.go | 22 +-- db/models/publicreport.image.bob.go | 22 +-- db/models/publicreport.image_exif.bob.go | 22 +-- db/models/publicreport.nuisance.bob.go | 22 +-- db/models/publicreport.pool.bob.go | 22 +-- db/models/publicreport.pool_image.bob.go | 22 +-- db/models/publicreport.quick.bob.go | 22 +-- db/models/publicreport.quick_image.bob.go | 22 +-- db/models/publicreport.report_location.bob.go | 8 +- db/models/raster_columns.bob.go | 8 +- db/models/raster_overviews.bob.go | 8 +- db/models/sessions.bob.go | 16 +- db/models/spatial_ref_sys.bob.go | 16 +- db/models/user_.bob.go | 22 +-- db/prepared.go | 6 +- db/sql/org_by_oauth_id.bob.go | 10 +- db/sql/org_by_oauth_id.bob.sql | 2 +- ...creport_image_with_json_by_quick_id.bob.go | 10 +- ...report_image_with_json_by_quick_id.bob.sql | 2 +- db/sql/publicreport_publicid_table.bob.go | 10 +- db/sql/publicreport_publicid_table.bob.sql | 2 +- db/sql/texts_by_senders.bob.go | 56 +++--- db/sql/texts_by_senders.bob.sql | 3 +- db/sql/texts_by_senders.sql | 1 + db/sql/trapcount_by_location_id.bob.go | 12 +- db/sql/trapcount_by_location_id.bob.sql | 2 +- db/sql/trapdata_by_location_id_recent.bob.go | 12 +- db/sql/trapdata_by_location_id_recent.bob.sql | 2 +- db/sql/traplocation_by_source_id.bob.go | 10 +- db/sql/traplocation_by_source_id.bob.sql | 2 +- db/sql/update_oauth_org.bob.go | 10 +- db/sql/update_oauth_org.bob.sql | 2 +- db/sql/user_by_username.bob.go | 10 +- db/sql/user_by_username.bob.sql | 2 +- go.mod | 3 +- go.sum | 2 + llm/client.go | 56 +++++- llm/log.go | 11 +- llm/openai.go | 56 ++---- main.go | 3 +- platform/district.go | 4 +- platform/text.go | 102 +++++++++-- public-report/image-upload.go | 6 +- public-report/pool.go | 6 +- public-report/quick.go | 6 +- public-report/status.go | 6 +- query.go | 4 +- sync/dash.go | 2 +- sync/utils.go | 6 +- 302 files changed, 1428 insertions(+), 1256 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index f2f8d0b5..df88736b 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -8,6 +8,8 @@ import ( "strconv" "strings" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" @@ -16,8 +18,6 @@ import ( "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/sm" "golang.org/x/crypto/bcrypt" ) diff --git a/background/arcgis.go b/background/arcgis.go index 0e7e166a..c2a1f474 100644 --- a/background/arcgis.go +++ b/background/arcgis.go @@ -22,6 +22,10 @@ import ( "github.com/Gleipnir-Technology/arcgis-go" "github.com/Gleipnir-Technology/arcgis-go/fieldseeker" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" @@ -33,10 +37,6 @@ import ( "github.com/alitto/pond/v2" "github.com/jackc/pgx/v5" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" ) var syncStatusByOrg map[int32]bool diff --git a/background/summary.go b/background/summary.go index dc6b701f..874c56c8 100644 --- a/background/summary.go +++ b/background/summary.go @@ -4,16 +4,16 @@ import ( "context" "fmt" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/im" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/h3utils" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/im" "github.com/uber/h3-go/v4" ) diff --git a/comms/email/db.go b/comms/email/db.go index 39d4dc59..d12e9316 100644 --- a/comms/email/db.go +++ b/comms/email/db.go @@ -10,13 +10,13 @@ import ( "strings" "time" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob/types/pgtypes" ) func convertToPGData(data map[string]string) pgtypes.HStore { diff --git a/comms/email/template.go b/comms/email/template.go index 424619f6..adb6446f 100644 --- a/comms/email/template.go +++ b/comms/email/template.go @@ -16,15 +16,15 @@ import ( templatetxt "text/template" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/um" ) //go:embed template/* diff --git a/comms/text/db.go b/comms/text/db.go index cd6c3d7d..47a58c0e 100644 --- a/comms/text/db.go +++ b/comms/text/db.go @@ -8,8 +8,8 @@ import ( "sort" "strings" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/Gleipnir-Technology/nidus-sync/db/enums" - "github.com/stephenafamo/bob/types/pgtypes" ) func convertToPGData(data map[string]string) pgtypes.HStore { diff --git a/db/connection.go b/db/connection.go index cc6924e4..7636dd4a 100644 --- a/db/connection.go +++ b/db/connection.go @@ -11,12 +11,12 @@ import ( //"github.com/georgysavva/scany/v2/pgxscan" //"github.com/jackc/pgx/v5" + "github.com/Gleipnir-Technology/bob" "github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/stdlib" _ "github.com/jackc/pgx/v5/stdlib" "github.com/pressly/goose/v3" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob" ) //go:embed migrations/*.sql diff --git a/db/dberrors/arcgis.user_.bob.go b/db/dberrors/arcgis.user_.bob.go index d17b1fef..ecdd2bf4 100644 --- a/db/dberrors/arcgis.user_.bob.go +++ b/db/dberrors/arcgis.user_.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/arcgis.user_privilege.bob.go b/db/dberrors/arcgis.user_privilege.bob.go index 7ff6253f..1539a638 100644 --- a/db/dberrors/arcgis.user_privilege.bob.go +++ b/db/dberrors/arcgis.user_privilege.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/bob_errors.bob.go b/db/dberrors/bob_errors.bob.go index ec4056c3..c236b601 100644 --- a/db/dberrors/bob_errors.bob.go +++ b/db/dberrors/bob_errors.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/comms.email_contact.bob.go b/db/dberrors/comms.email_contact.bob.go index 08568a2c..fab88251 100644 --- a/db/dberrors/comms.email_contact.bob.go +++ b/db/dberrors/comms.email_contact.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/comms.email_log.bob.go b/db/dberrors/comms.email_log.bob.go index 29f8370c..ed3d2ceb 100644 --- a/db/dberrors/comms.email_log.bob.go +++ b/db/dberrors/comms.email_log.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/comms.email_template.bob.go b/db/dberrors/comms.email_template.bob.go index d0fe76ed..d1cf47f5 100644 --- a/db/dberrors/comms.email_template.bob.go +++ b/db/dberrors/comms.email_template.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/comms.phone.bob.go b/db/dberrors/comms.phone.bob.go index ee7af99c..bf0cb617 100644 --- a/db/dberrors/comms.phone.bob.go +++ b/db/dberrors/comms.phone.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/comms.text_job.bob.go b/db/dberrors/comms.text_job.bob.go index cedcca8e..361daa57 100644 --- a/db/dberrors/comms.text_job.bob.go +++ b/db/dberrors/comms.text_job.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/comms.text_log.bob.go b/db/dberrors/comms.text_log.bob.go index 541e7178..11377aff 100644 --- a/db/dberrors/comms.text_log.bob.go +++ b/db/dberrors/comms.text_log.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.containerrelate.bob.go b/db/dberrors/fieldseeker.containerrelate.bob.go index 40d01312..e5a7e4f9 100644 --- a/db/dberrors/fieldseeker.containerrelate.bob.go +++ b/db/dberrors/fieldseeker.containerrelate.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.fieldscoutinglog.bob.go b/db/dberrors/fieldseeker.fieldscoutinglog.bob.go index 3237f5db..43b8f9aa 100644 --- a/db/dberrors/fieldseeker.fieldscoutinglog.bob.go +++ b/db/dberrors/fieldseeker.fieldscoutinglog.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.habitatrelate.bob.go b/db/dberrors/fieldseeker.habitatrelate.bob.go index d00f431c..20046415 100644 --- a/db/dberrors/fieldseeker.habitatrelate.bob.go +++ b/db/dberrors/fieldseeker.habitatrelate.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.inspectionsample.bob.go b/db/dberrors/fieldseeker.inspectionsample.bob.go index ce6725a5..2b247029 100644 --- a/db/dberrors/fieldseeker.inspectionsample.bob.go +++ b/db/dberrors/fieldseeker.inspectionsample.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.inspectionsampledetail.bob.go b/db/dberrors/fieldseeker.inspectionsampledetail.bob.go index 69d7b6e6..1bbb1035 100644 --- a/db/dberrors/fieldseeker.inspectionsampledetail.bob.go +++ b/db/dberrors/fieldseeker.inspectionsampledetail.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.linelocation.bob.go b/db/dberrors/fieldseeker.linelocation.bob.go index 5a135a53..742fbfa2 100644 --- a/db/dberrors/fieldseeker.linelocation.bob.go +++ b/db/dberrors/fieldseeker.linelocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.locationtracking.bob.go b/db/dberrors/fieldseeker.locationtracking.bob.go index e68d1172..ccaa77c6 100644 --- a/db/dberrors/fieldseeker.locationtracking.bob.go +++ b/db/dberrors/fieldseeker.locationtracking.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.mosquitoinspection.bob.go b/db/dberrors/fieldseeker.mosquitoinspection.bob.go index f9b119a7..f456a2e0 100644 --- a/db/dberrors/fieldseeker.mosquitoinspection.bob.go +++ b/db/dberrors/fieldseeker.mosquitoinspection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.pointlocation.bob.go b/db/dberrors/fieldseeker.pointlocation.bob.go index 9078a437..831398ca 100644 --- a/db/dberrors/fieldseeker.pointlocation.bob.go +++ b/db/dberrors/fieldseeker.pointlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.polygonlocation.bob.go b/db/dberrors/fieldseeker.polygonlocation.bob.go index 250e5de2..ade05089 100644 --- a/db/dberrors/fieldseeker.polygonlocation.bob.go +++ b/db/dberrors/fieldseeker.polygonlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.pool.bob.go b/db/dberrors/fieldseeker.pool.bob.go index 7bb30e90..fd1eb1d9 100644 --- a/db/dberrors/fieldseeker.pool.bob.go +++ b/db/dberrors/fieldseeker.pool.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.pooldetail.bob.go b/db/dberrors/fieldseeker.pooldetail.bob.go index 1423b462..fb713106 100644 --- a/db/dberrors/fieldseeker.pooldetail.bob.go +++ b/db/dberrors/fieldseeker.pooldetail.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.proposedtreatmentarea.bob.go b/db/dberrors/fieldseeker.proposedtreatmentarea.bob.go index 991a4d46..2dfb4338 100644 --- a/db/dberrors/fieldseeker.proposedtreatmentarea.bob.go +++ b/db/dberrors/fieldseeker.proposedtreatmentarea.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.qamosquitoinspection.bob.go b/db/dberrors/fieldseeker.qamosquitoinspection.bob.go index 5ae42a56..9176d95e 100644 --- a/db/dberrors/fieldseeker.qamosquitoinspection.bob.go +++ b/db/dberrors/fieldseeker.qamosquitoinspection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.rodentlocation.bob.go b/db/dberrors/fieldseeker.rodentlocation.bob.go index d865425f..7513df7a 100644 --- a/db/dberrors/fieldseeker.rodentlocation.bob.go +++ b/db/dberrors/fieldseeker.rodentlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.samplecollection.bob.go b/db/dberrors/fieldseeker.samplecollection.bob.go index 6f0de912..874a33d2 100644 --- a/db/dberrors/fieldseeker.samplecollection.bob.go +++ b/db/dberrors/fieldseeker.samplecollection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.samplelocation.bob.go b/db/dberrors/fieldseeker.samplelocation.bob.go index 35a96bc1..1c1d72d9 100644 --- a/db/dberrors/fieldseeker.samplelocation.bob.go +++ b/db/dberrors/fieldseeker.samplelocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.servicerequest.bob.go b/db/dberrors/fieldseeker.servicerequest.bob.go index 5e8d1020..ceaa44d6 100644 --- a/db/dberrors/fieldseeker.servicerequest.bob.go +++ b/db/dberrors/fieldseeker.servicerequest.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.speciesabundance.bob.go b/db/dberrors/fieldseeker.speciesabundance.bob.go index 922bcc2a..e9bc8e6d 100644 --- a/db/dberrors/fieldseeker.speciesabundance.bob.go +++ b/db/dberrors/fieldseeker.speciesabundance.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.stormdrain.bob.go b/db/dberrors/fieldseeker.stormdrain.bob.go index 689fe28d..1fc22909 100644 --- a/db/dberrors/fieldseeker.stormdrain.bob.go +++ b/db/dberrors/fieldseeker.stormdrain.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.timecard.bob.go b/db/dberrors/fieldseeker.timecard.bob.go index bf09f766..abc16c3d 100644 --- a/db/dberrors/fieldseeker.timecard.bob.go +++ b/db/dberrors/fieldseeker.timecard.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.trapdata.bob.go b/db/dberrors/fieldseeker.trapdata.bob.go index 7ffe6603..312ec967 100644 --- a/db/dberrors/fieldseeker.trapdata.bob.go +++ b/db/dberrors/fieldseeker.trapdata.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.traplocation.bob.go b/db/dberrors/fieldseeker.traplocation.bob.go index de57fa80..45ac70f1 100644 --- a/db/dberrors/fieldseeker.traplocation.bob.go +++ b/db/dberrors/fieldseeker.traplocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.treatment.bob.go b/db/dberrors/fieldseeker.treatment.bob.go index 40e0bddf..768ec5c0 100644 --- a/db/dberrors/fieldseeker.treatment.bob.go +++ b/db/dberrors/fieldseeker.treatment.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.treatmentarea.bob.go b/db/dberrors/fieldseeker.treatmentarea.bob.go index c7ff263b..3bf5ff44 100644 --- a/db/dberrors/fieldseeker.treatmentarea.bob.go +++ b/db/dberrors/fieldseeker.treatmentarea.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.zones.bob.go b/db/dberrors/fieldseeker.zones.bob.go index bd2fd6e6..6ba01fe8 100644 --- a/db/dberrors/fieldseeker.zones.bob.go +++ b/db/dberrors/fieldseeker.zones.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker.zones2.bob.go b/db/dberrors/fieldseeker.zones2.bob.go index 644248d7..eb77f353 100644 --- a/db/dberrors/fieldseeker.zones2.bob.go +++ b/db/dberrors/fieldseeker.zones2.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/fieldseeker_sync.bob.go b/db/dberrors/fieldseeker_sync.bob.go index ac899d66..5f412382 100644 --- a/db/dberrors/fieldseeker_sync.bob.go +++ b/db/dberrors/fieldseeker_sync.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/goose_db_version.bob.go b/db/dberrors/goose_db_version.bob.go index af370d75..05367ee2 100644 --- a/db/dberrors/goose_db_version.bob.go +++ b/db/dberrors/goose_db_version.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/h3_aggregation.bob.go b/db/dberrors/h3_aggregation.bob.go index 80c555b1..e3b34027 100644 --- a/db/dberrors/h3_aggregation.bob.go +++ b/db/dberrors/h3_aggregation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/import.district.bob.go b/db/dberrors/import.district.bob.go index a69509a5..61d0df39 100644 --- a/db/dberrors/import.district.bob.go +++ b/db/dberrors/import.district.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/note_audio.bob.go b/db/dberrors/note_audio.bob.go index 51621255..da314876 100644 --- a/db/dberrors/note_audio.bob.go +++ b/db/dberrors/note_audio.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/note_audio_breadcrumb.bob.go b/db/dberrors/note_audio_breadcrumb.bob.go index c0db7c25..4b642bf5 100644 --- a/db/dberrors/note_audio_breadcrumb.bob.go +++ b/db/dberrors/note_audio_breadcrumb.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/note_audio_data.bob.go b/db/dberrors/note_audio_data.bob.go index bd4b07c1..a04b251a 100644 --- a/db/dberrors/note_audio_data.bob.go +++ b/db/dberrors/note_audio_data.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/note_image.bob.go b/db/dberrors/note_image.bob.go index 8090f936..bc6f8f40 100644 --- a/db/dberrors/note_image.bob.go +++ b/db/dberrors/note_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/note_image_breadcrumb.bob.go b/db/dberrors/note_image_breadcrumb.bob.go index 7e62d2ad..bc5fef56 100644 --- a/db/dberrors/note_image_breadcrumb.bob.go +++ b/db/dberrors/note_image_breadcrumb.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/note_image_data.bob.go b/db/dberrors/note_image_data.bob.go index 91c1a89a..62cfa026 100644 --- a/db/dberrors/note_image_data.bob.go +++ b/db/dberrors/note_image_data.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/notification.bob.go b/db/dberrors/notification.bob.go index d4f2841a..e32540ef 100644 --- a/db/dberrors/notification.bob.go +++ b/db/dberrors/notification.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/oauth_token.bob.go b/db/dberrors/oauth_token.bob.go index 050b1a47..b1b15e6b 100644 --- a/db/dberrors/oauth_token.bob.go +++ b/db/dberrors/oauth_token.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/organization.bob.go b/db/dberrors/organization.bob.go index d3199621..d01bcc41 100644 --- a/db/dberrors/organization.bob.go +++ b/db/dberrors/organization.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/publicreport.image.bob.go b/db/dberrors/publicreport.image.bob.go index 4986333c..4bc0270a 100644 --- a/db/dberrors/publicreport.image.bob.go +++ b/db/dberrors/publicreport.image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/publicreport.image_exif.bob.go b/db/dberrors/publicreport.image_exif.bob.go index 28ab3ada..41de3c31 100644 --- a/db/dberrors/publicreport.image_exif.bob.go +++ b/db/dberrors/publicreport.image_exif.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/publicreport.nuisance.bob.go b/db/dberrors/publicreport.nuisance.bob.go index c1fcebb0..6c0fd841 100644 --- a/db/dberrors/publicreport.nuisance.bob.go +++ b/db/dberrors/publicreport.nuisance.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/publicreport.pool.bob.go b/db/dberrors/publicreport.pool.bob.go index aefb420d..c774c014 100644 --- a/db/dberrors/publicreport.pool.bob.go +++ b/db/dberrors/publicreport.pool.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/publicreport.pool_image.bob.go b/db/dberrors/publicreport.pool_image.bob.go index 5317ef9f..8f5c66e2 100644 --- a/db/dberrors/publicreport.pool_image.bob.go +++ b/db/dberrors/publicreport.pool_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/publicreport.quick.bob.go b/db/dberrors/publicreport.quick.bob.go index d51ed705..e00cd069 100644 --- a/db/dberrors/publicreport.quick.bob.go +++ b/db/dberrors/publicreport.quick.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/publicreport.quick_image.bob.go b/db/dberrors/publicreport.quick_image.bob.go index d0bb4097..d66ae0a3 100644 --- a/db/dberrors/publicreport.quick_image.bob.go +++ b/db/dberrors/publicreport.quick_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/sessions.bob.go b/db/dberrors/sessions.bob.go index e271ce05..c27ab6b7 100644 --- a/db/dberrors/sessions.bob.go +++ b/db/dberrors/sessions.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/spatial_ref_sys.bob.go b/db/dberrors/spatial_ref_sys.bob.go index 195ac12d..b3a777fd 100644 --- a/db/dberrors/spatial_ref_sys.bob.go +++ b/db/dberrors/spatial_ref_sys.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dberrors/user_.bob.go b/db/dberrors/user_.bob.go index 7f08ae62..f1d6fbb1 100644 --- a/db/dberrors/user_.bob.go +++ b/db/dberrors/user_.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dberrors diff --git a/db/dbinfo/arcgis.user_.bob.go b/db/dbinfo/arcgis.user_.bob.go index 772f92db..0e70ba47 100644 --- a/db/dbinfo/arcgis.user_.bob.go +++ b/db/dbinfo/arcgis.user_.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/arcgis.user_privilege.bob.go b/db/dbinfo/arcgis.user_privilege.bob.go index 88e2785f..56de66b9 100644 --- a/db/dbinfo/arcgis.user_privilege.bob.go +++ b/db/dbinfo/arcgis.user_privilege.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/bob_types.bob.go b/db/dbinfo/bob_types.bob.go index 2ece4dab..0337bf04 100644 --- a/db/dbinfo/bob_types.bob.go +++ b/db/dbinfo/bob_types.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/comms.email_contact.bob.go b/db/dbinfo/comms.email_contact.bob.go index 6dff7493..fdb1bde5 100644 --- a/db/dbinfo/comms.email_contact.bob.go +++ b/db/dbinfo/comms.email_contact.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/comms.email_log.bob.go b/db/dbinfo/comms.email_log.bob.go index 09a41454..09417e10 100644 --- a/db/dbinfo/comms.email_log.bob.go +++ b/db/dbinfo/comms.email_log.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/comms.email_template.bob.go b/db/dbinfo/comms.email_template.bob.go index b299e2e7..a5615595 100644 --- a/db/dbinfo/comms.email_template.bob.go +++ b/db/dbinfo/comms.email_template.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/comms.phone.bob.go b/db/dbinfo/comms.phone.bob.go index be6039b2..d769ef1d 100644 --- a/db/dbinfo/comms.phone.bob.go +++ b/db/dbinfo/comms.phone.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/comms.text_job.bob.go b/db/dbinfo/comms.text_job.bob.go index aa87ea6f..cd2dfabd 100644 --- a/db/dbinfo/comms.text_job.bob.go +++ b/db/dbinfo/comms.text_job.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/comms.text_log.bob.go b/db/dbinfo/comms.text_log.bob.go index d85d75d6..f01f10bb 100644 --- a/db/dbinfo/comms.text_log.bob.go +++ b/db/dbinfo/comms.text_log.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo @@ -96,6 +96,15 @@ var CommsTextLogs = Table[ Generated: false, AutoIncr: false, }, + IsVisibleToLLM: column{ + Name: "is_visible_to_llm", + DBType: "boolean", + Default: "", + Comment: "", + Nullable: false, + Generated: false, + AutoIncr: false, + }, }, Indexes: commsTextLogIndexes{ TextLogPkey: index{ @@ -170,20 +179,21 @@ var CommsTextLogs = Table[ } type commsTextLogColumns struct { - Content column - Created column - Destination column - ID column - IsWelcome column - Origin column - Source column - TwilioSid column - TwilioStatus column + Content column + Created column + Destination column + ID column + IsWelcome column + Origin column + Source column + TwilioSid column + TwilioStatus column + IsVisibleToLLM column } func (c commsTextLogColumns) AsSlice() []column { return []column{ - c.Content, c.Created, c.Destination, c.ID, c.IsWelcome, c.Origin, c.Source, c.TwilioSid, c.TwilioStatus, + c.Content, c.Created, c.Destination, c.ID, c.IsWelcome, c.Origin, c.Source, c.TwilioSid, c.TwilioStatus, c.IsVisibleToLLM, } } diff --git a/db/dbinfo/fieldseeker.containerrelate.bob.go b/db/dbinfo/fieldseeker.containerrelate.bob.go index 9b293435..c41dbb5a 100644 --- a/db/dbinfo/fieldseeker.containerrelate.bob.go +++ b/db/dbinfo/fieldseeker.containerrelate.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.fieldscoutinglog.bob.go b/db/dbinfo/fieldseeker.fieldscoutinglog.bob.go index 7a8b2e88..21b7ae92 100644 --- a/db/dbinfo/fieldseeker.fieldscoutinglog.bob.go +++ b/db/dbinfo/fieldseeker.fieldscoutinglog.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.habitatrelate.bob.go b/db/dbinfo/fieldseeker.habitatrelate.bob.go index 5b7eef2f..a2229e90 100644 --- a/db/dbinfo/fieldseeker.habitatrelate.bob.go +++ b/db/dbinfo/fieldseeker.habitatrelate.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.inspectionsample.bob.go b/db/dbinfo/fieldseeker.inspectionsample.bob.go index 7222c516..e7e34fca 100644 --- a/db/dbinfo/fieldseeker.inspectionsample.bob.go +++ b/db/dbinfo/fieldseeker.inspectionsample.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.inspectionsampledetail.bob.go b/db/dbinfo/fieldseeker.inspectionsampledetail.bob.go index 9bfee61f..8b32c98b 100644 --- a/db/dbinfo/fieldseeker.inspectionsampledetail.bob.go +++ b/db/dbinfo/fieldseeker.inspectionsampledetail.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.linelocation.bob.go b/db/dbinfo/fieldseeker.linelocation.bob.go index 426609fa..81cd0549 100644 --- a/db/dbinfo/fieldseeker.linelocation.bob.go +++ b/db/dbinfo/fieldseeker.linelocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.locationtracking.bob.go b/db/dbinfo/fieldseeker.locationtracking.bob.go index fa3d2922..091413c6 100644 --- a/db/dbinfo/fieldseeker.locationtracking.bob.go +++ b/db/dbinfo/fieldseeker.locationtracking.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.mosquitoinspection.bob.go b/db/dbinfo/fieldseeker.mosquitoinspection.bob.go index 333ae452..aadf5c34 100644 --- a/db/dbinfo/fieldseeker.mosquitoinspection.bob.go +++ b/db/dbinfo/fieldseeker.mosquitoinspection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.pointlocation.bob.go b/db/dbinfo/fieldseeker.pointlocation.bob.go index 5f65e44a..6504254d 100644 --- a/db/dbinfo/fieldseeker.pointlocation.bob.go +++ b/db/dbinfo/fieldseeker.pointlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.polygonlocation.bob.go b/db/dbinfo/fieldseeker.polygonlocation.bob.go index e200e6c7..471fb5ca 100644 --- a/db/dbinfo/fieldseeker.polygonlocation.bob.go +++ b/db/dbinfo/fieldseeker.polygonlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.pool.bob.go b/db/dbinfo/fieldseeker.pool.bob.go index 9f359745..d92b0a47 100644 --- a/db/dbinfo/fieldseeker.pool.bob.go +++ b/db/dbinfo/fieldseeker.pool.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.pooldetail.bob.go b/db/dbinfo/fieldseeker.pooldetail.bob.go index b3a9e700..930442a3 100644 --- a/db/dbinfo/fieldseeker.pooldetail.bob.go +++ b/db/dbinfo/fieldseeker.pooldetail.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.proposedtreatmentarea.bob.go b/db/dbinfo/fieldseeker.proposedtreatmentarea.bob.go index f85288c6..518e08c4 100644 --- a/db/dbinfo/fieldseeker.proposedtreatmentarea.bob.go +++ b/db/dbinfo/fieldseeker.proposedtreatmentarea.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.qamosquitoinspection.bob.go b/db/dbinfo/fieldseeker.qamosquitoinspection.bob.go index 76da7dd7..b34a6a93 100644 --- a/db/dbinfo/fieldseeker.qamosquitoinspection.bob.go +++ b/db/dbinfo/fieldseeker.qamosquitoinspection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.rodentlocation.bob.go b/db/dbinfo/fieldseeker.rodentlocation.bob.go index 8713d101..7e7e16f5 100644 --- a/db/dbinfo/fieldseeker.rodentlocation.bob.go +++ b/db/dbinfo/fieldseeker.rodentlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.samplecollection.bob.go b/db/dbinfo/fieldseeker.samplecollection.bob.go index a205203b..c91d1b83 100644 --- a/db/dbinfo/fieldseeker.samplecollection.bob.go +++ b/db/dbinfo/fieldseeker.samplecollection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.samplelocation.bob.go b/db/dbinfo/fieldseeker.samplelocation.bob.go index 1f63b1c4..1d2aad35 100644 --- a/db/dbinfo/fieldseeker.samplelocation.bob.go +++ b/db/dbinfo/fieldseeker.samplelocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.servicerequest.bob.go b/db/dbinfo/fieldseeker.servicerequest.bob.go index 30de6afe..908cfc92 100644 --- a/db/dbinfo/fieldseeker.servicerequest.bob.go +++ b/db/dbinfo/fieldseeker.servicerequest.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.speciesabundance.bob.go b/db/dbinfo/fieldseeker.speciesabundance.bob.go index 7431468f..a3e2c508 100644 --- a/db/dbinfo/fieldseeker.speciesabundance.bob.go +++ b/db/dbinfo/fieldseeker.speciesabundance.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.stormdrain.bob.go b/db/dbinfo/fieldseeker.stormdrain.bob.go index 412832a3..f3f68c92 100644 --- a/db/dbinfo/fieldseeker.stormdrain.bob.go +++ b/db/dbinfo/fieldseeker.stormdrain.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.timecard.bob.go b/db/dbinfo/fieldseeker.timecard.bob.go index 65b6a792..fca3c1fb 100644 --- a/db/dbinfo/fieldseeker.timecard.bob.go +++ b/db/dbinfo/fieldseeker.timecard.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.trapdata.bob.go b/db/dbinfo/fieldseeker.trapdata.bob.go index 2c7c523f..4588e275 100644 --- a/db/dbinfo/fieldseeker.trapdata.bob.go +++ b/db/dbinfo/fieldseeker.trapdata.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.traplocation.bob.go b/db/dbinfo/fieldseeker.traplocation.bob.go index 27aa9951..a9bf2bf7 100644 --- a/db/dbinfo/fieldseeker.traplocation.bob.go +++ b/db/dbinfo/fieldseeker.traplocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.treatment.bob.go b/db/dbinfo/fieldseeker.treatment.bob.go index b6a0075e..0e4aec16 100644 --- a/db/dbinfo/fieldseeker.treatment.bob.go +++ b/db/dbinfo/fieldseeker.treatment.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.treatmentarea.bob.go b/db/dbinfo/fieldseeker.treatmentarea.bob.go index d9df5b58..7141b8b2 100644 --- a/db/dbinfo/fieldseeker.treatmentarea.bob.go +++ b/db/dbinfo/fieldseeker.treatmentarea.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.zones.bob.go b/db/dbinfo/fieldseeker.zones.bob.go index 1cd771cd..603514ae 100644 --- a/db/dbinfo/fieldseeker.zones.bob.go +++ b/db/dbinfo/fieldseeker.zones.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker.zones2.bob.go b/db/dbinfo/fieldseeker.zones2.bob.go index 7a55e0e8..eea38196 100644 --- a/db/dbinfo/fieldseeker.zones2.bob.go +++ b/db/dbinfo/fieldseeker.zones2.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/fieldseeker_sync.bob.go b/db/dbinfo/fieldseeker_sync.bob.go index f0d18f63..e5265867 100644 --- a/db/dbinfo/fieldseeker_sync.bob.go +++ b/db/dbinfo/fieldseeker_sync.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/geography_columns.bob.go b/db/dbinfo/geography_columns.bob.go index fede3218..5e450b0d 100644 --- a/db/dbinfo/geography_columns.bob.go +++ b/db/dbinfo/geography_columns.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/geometry_columns.bob.go b/db/dbinfo/geometry_columns.bob.go index 2292ee9a..b28a0ded 100644 --- a/db/dbinfo/geometry_columns.bob.go +++ b/db/dbinfo/geometry_columns.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/goose_db_version.bob.go b/db/dbinfo/goose_db_version.bob.go index 9bda0126..3b4889fe 100644 --- a/db/dbinfo/goose_db_version.bob.go +++ b/db/dbinfo/goose_db_version.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/h3_aggregation.bob.go b/db/dbinfo/h3_aggregation.bob.go index 65bd6414..e2c5ae90 100644 --- a/db/dbinfo/h3_aggregation.bob.go +++ b/db/dbinfo/h3_aggregation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/import.district.bob.go b/db/dbinfo/import.district.bob.go index e51c4fcb..cefc73ed 100644 --- a/db/dbinfo/import.district.bob.go +++ b/db/dbinfo/import.district.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/note_audio.bob.go b/db/dbinfo/note_audio.bob.go index 4725221d..3046131f 100644 --- a/db/dbinfo/note_audio.bob.go +++ b/db/dbinfo/note_audio.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/note_audio_breadcrumb.bob.go b/db/dbinfo/note_audio_breadcrumb.bob.go index 0f6e5248..b71e6b31 100644 --- a/db/dbinfo/note_audio_breadcrumb.bob.go +++ b/db/dbinfo/note_audio_breadcrumb.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/note_audio_data.bob.go b/db/dbinfo/note_audio_data.bob.go index 8e3dae3f..cf7b22d2 100644 --- a/db/dbinfo/note_audio_data.bob.go +++ b/db/dbinfo/note_audio_data.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/note_image.bob.go b/db/dbinfo/note_image.bob.go index 89a0dccd..2a48e71a 100644 --- a/db/dbinfo/note_image.bob.go +++ b/db/dbinfo/note_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/note_image_breadcrumb.bob.go b/db/dbinfo/note_image_breadcrumb.bob.go index 7824d997..8492f301 100644 --- a/db/dbinfo/note_image_breadcrumb.bob.go +++ b/db/dbinfo/note_image_breadcrumb.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/note_image_data.bob.go b/db/dbinfo/note_image_data.bob.go index dade46b3..53bbeb23 100644 --- a/db/dbinfo/note_image_data.bob.go +++ b/db/dbinfo/note_image_data.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/notification.bob.go b/db/dbinfo/notification.bob.go index b405ed17..39c98e12 100644 --- a/db/dbinfo/notification.bob.go +++ b/db/dbinfo/notification.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/oauth_token.bob.go b/db/dbinfo/oauth_token.bob.go index e7ae9dcf..3101f04e 100644 --- a/db/dbinfo/oauth_token.bob.go +++ b/db/dbinfo/oauth_token.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/organization.bob.go b/db/dbinfo/organization.bob.go index da9cf320..8248f9fc 100644 --- a/db/dbinfo/organization.bob.go +++ b/db/dbinfo/organization.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/publicreport.image.bob.go b/db/dbinfo/publicreport.image.bob.go index 231da24f..987af228 100644 --- a/db/dbinfo/publicreport.image.bob.go +++ b/db/dbinfo/publicreport.image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/publicreport.image_exif.bob.go b/db/dbinfo/publicreport.image_exif.bob.go index 97aa27b7..ab70ef12 100644 --- a/db/dbinfo/publicreport.image_exif.bob.go +++ b/db/dbinfo/publicreport.image_exif.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/publicreport.nuisance.bob.go b/db/dbinfo/publicreport.nuisance.bob.go index 20debe36..e8d1cad6 100644 --- a/db/dbinfo/publicreport.nuisance.bob.go +++ b/db/dbinfo/publicreport.nuisance.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/publicreport.pool.bob.go b/db/dbinfo/publicreport.pool.bob.go index a749fdba..dc985a82 100644 --- a/db/dbinfo/publicreport.pool.bob.go +++ b/db/dbinfo/publicreport.pool.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/publicreport.pool_image.bob.go b/db/dbinfo/publicreport.pool_image.bob.go index cb44e9b9..3d39ed61 100644 --- a/db/dbinfo/publicreport.pool_image.bob.go +++ b/db/dbinfo/publicreport.pool_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/publicreport.quick.bob.go b/db/dbinfo/publicreport.quick.bob.go index a986b5b8..74d3d8ec 100644 --- a/db/dbinfo/publicreport.quick.bob.go +++ b/db/dbinfo/publicreport.quick.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/publicreport.quick_image.bob.go b/db/dbinfo/publicreport.quick_image.bob.go index d311615b..1615d4ee 100644 --- a/db/dbinfo/publicreport.quick_image.bob.go +++ b/db/dbinfo/publicreport.quick_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/publicreport.report_location.bob.go b/db/dbinfo/publicreport.report_location.bob.go index 92fa50fe..d458deff 100644 --- a/db/dbinfo/publicreport.report_location.bob.go +++ b/db/dbinfo/publicreport.report_location.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/raster_columns.bob.go b/db/dbinfo/raster_columns.bob.go index a7d56223..e81669dd 100644 --- a/db/dbinfo/raster_columns.bob.go +++ b/db/dbinfo/raster_columns.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/raster_overviews.bob.go b/db/dbinfo/raster_overviews.bob.go index 7199817b..1c1a6e9e 100644 --- a/db/dbinfo/raster_overviews.bob.go +++ b/db/dbinfo/raster_overviews.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/sessions.bob.go b/db/dbinfo/sessions.bob.go index 4f66b5ce..c18de5c4 100644 --- a/db/dbinfo/sessions.bob.go +++ b/db/dbinfo/sessions.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/spatial_ref_sys.bob.go b/db/dbinfo/spatial_ref_sys.bob.go index 78b5a363..a28829c4 100644 --- a/db/dbinfo/spatial_ref_sys.bob.go +++ b/db/dbinfo/spatial_ref_sys.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/dbinfo/user_.bob.go b/db/dbinfo/user_.bob.go index 1c1f25ef..44fdd868 100644 --- a/db/dbinfo/user_.bob.go +++ b/db/dbinfo/user_.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package dbinfo diff --git a/db/enums/enums.bob.go b/db/enums/enums.bob.go index cadb022f..66d39176 100644 --- a/db/enums/enums.bob.go +++ b/db/enums/enums.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package enums @@ -344,11 +344,12 @@ func (e *CommsTextjobtype) Scan(value any) error { // Enum values for CommsTextorigin const ( - CommsTextoriginDistrict CommsTextorigin = "district" - CommsTextoriginLLM CommsTextorigin = "llm" - CommsTextoriginWebsiteAction CommsTextorigin = "website-action" - CommsTextoriginCustomer CommsTextorigin = "customer" - CommsTextoriginReiteration CommsTextorigin = "reiteration" + CommsTextoriginDistrict CommsTextorigin = "district" + CommsTextoriginLLM CommsTextorigin = "llm" + CommsTextoriginWebsiteAction CommsTextorigin = "website-action" + CommsTextoriginCustomer CommsTextorigin = "customer" + CommsTextoriginReiteration CommsTextorigin = "reiteration" + CommsTextoriginCommandResponse CommsTextorigin = "command-response" ) func AllCommsTextorigin() []CommsTextorigin { @@ -358,6 +359,7 @@ func AllCommsTextorigin() []CommsTextorigin { CommsTextoriginWebsiteAction, CommsTextoriginCustomer, CommsTextoriginReiteration, + CommsTextoriginCommandResponse, } } @@ -373,7 +375,8 @@ func (e CommsTextorigin) Valid() bool { CommsTextoriginLLM, CommsTextoriginWebsiteAction, CommsTextoriginCustomer, - CommsTextoriginReiteration: + CommsTextoriginReiteration, + CommsTextoriginCommandResponse: return true default: return false diff --git a/db/factory/arcgis.user_.bob.go b/db/factory/arcgis.user_.bob.go index 43969366..51851826 100644 --- a/db/factory/arcgis.user_.bob.go +++ b/db/factory/arcgis.user_.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,10 +8,10 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type ArcgisUserMod interface { diff --git a/db/factory/arcgis.user_privilege.bob.go b/db/factory/arcgis.user_privilege.bob.go index 37ed5e39..0fe57443 100644 --- a/db/factory/arcgis.user_privilege.bob.go +++ b/db/factory/arcgis.user_privilege.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,10 +7,10 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type ArcgisUserPrivilegeMod interface { diff --git a/db/factory/bobfactory_context.bob.go b/db/factory/bobfactory_context.bob.go index 4258ed4f..0643830a 100644 --- a/db/factory/bobfactory_context.bob.go +++ b/db/factory/bobfactory_context.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index f8f5ca98..cd749e18 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,14 +8,14 @@ import ( "encoding/json" "time" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/google/uuid" "github.com/lib/pq" "github.com/shopspring/decimal" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) type Factory struct { @@ -370,6 +370,7 @@ func (f *Factory) FromExistingCommsTextLog(m *models.CommsTextLog) *CommsTextLog o.Source = func() string { return m.Source } o.TwilioSid = func() null.Val[string] { return m.TwilioSid } o.TwilioStatus = func() string { return m.TwilioStatus } + o.IsVisibleToLLM = func() bool { return m.IsVisibleToLLM } ctx := context.Background() if m.R.DestinationPhone != nil { diff --git a/db/factory/bobfactory_random.bob.go b/db/factory/bobfactory_random.bob.go index 28ceac86..fe4997bf 100644 --- a/db/factory/bobfactory_random.bob.go +++ b/db/factory/bobfactory_random.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -13,13 +13,13 @@ import ( "strings" "time" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/google/uuid" "github.com/jaswdr/faker/v2" "github.com/lib/pq" "github.com/shopspring/decimal" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) var defaultFaker = faker.New() diff --git a/db/factory/comms.email_contact.bob.go b/db/factory/comms.email_contact.bob.go index 11a7fe1d..a0ca1038 100644 --- a/db/factory/comms.email_contact.bob.go +++ b/db/factory/comms.email_contact.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,10 +7,10 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type CommsEmailContactMod interface { diff --git a/db/factory/comms.email_log.bob.go b/db/factory/comms.email_log.bob.go index 13d3e040..04e52de6 100644 --- a/db/factory/comms.email_log.bob.go +++ b/db/factory/comms.email_log.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,14 +8,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types/pgtypes" ) type CommsEmailLogMod interface { diff --git a/db/factory/comms.email_template.bob.go b/db/factory/comms.email_template.bob.go index dbfdd1f7..a40c2f68 100644 --- a/db/factory/comms.email_template.bob.go +++ b/db/factory/comms.email_template.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type CommsEmailTemplateMod interface { diff --git a/db/factory/comms.phone.bob.go b/db/factory/comms.phone.bob.go index abcd554d..5c5d9aec 100644 --- a/db/factory/comms.phone.bob.go +++ b/db/factory/comms.phone.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,12 +7,12 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type CommsPhoneMod interface { diff --git a/db/factory/comms.text_job.bob.go b/db/factory/comms.text_job.bob.go index 6d16388f..1c62de8e 100644 --- a/db/factory/comms.text_job.bob.go +++ b/db/factory/comms.text_job.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,11 +8,11 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type CommsTextJobMod interface { diff --git a/db/factory/comms.text_log.bob.go b/db/factory/comms.text_log.bob.go index 5edb4b92..b25a4a43 100644 --- a/db/factory/comms.text_log.bob.go +++ b/db/factory/comms.text_log.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type CommsTextLogMod interface { @@ -38,15 +38,16 @@ func (mods CommsTextLogModSlice) Apply(ctx context.Context, n *CommsTextLogTempl // CommsTextLogTemplate is an object representing the database table. // all columns are optional and should be set by mods type CommsTextLogTemplate struct { - Content func() string - Created func() time.Time - Destination func() string - ID func() int32 - IsWelcome func() bool - Origin func() enums.CommsTextorigin - Source func() string - TwilioSid func() null.Val[string] - TwilioStatus func() string + Content func() string + Created func() time.Time + Destination func() string + ID func() int32 + IsWelcome func() bool + Origin func() enums.CommsTextorigin + Source func() string + TwilioSid func() null.Val[string] + TwilioStatus func() string + IsVisibleToLLM func() bool r commsTextLogR f *Factory @@ -132,6 +133,10 @@ func (o CommsTextLogTemplate) BuildSetter() *models.CommsTextLogSetter { val := o.TwilioStatus() m.TwilioStatus = omit.From(val) } + if o.IsVisibleToLLM != nil { + val := o.IsVisibleToLLM() + m.IsVisibleToLLM = omit.From(val) + } return m } @@ -181,6 +186,9 @@ func (o CommsTextLogTemplate) Build() *models.CommsTextLog { if o.TwilioStatus != nil { m.TwilioStatus = o.TwilioStatus() } + if o.IsVisibleToLLM != nil { + m.IsVisibleToLLM = o.IsVisibleToLLM() + } o.setModelRels(m) @@ -229,6 +237,10 @@ func ensureCreatableCommsTextLog(m *models.CommsTextLogSetter) { val := random_string(nil) m.TwilioStatus = omit.From(val) } + if !(m.IsVisibleToLLM.IsValue()) { + val := random_bool(nil) + m.IsVisibleToLLM = omit.From(val) + } } // insertOptRels creates and inserts any optional the relationships on *models.CommsTextLog @@ -375,6 +387,7 @@ func (m commsTextLogMods) RandomizeAllColumns(f *faker.Faker) CommsTextLogMod { CommsTextLogMods.RandomSource(f), CommsTextLogMods.RandomTwilioSid(f), CommsTextLogMods.RandomTwilioStatus(f), + CommsTextLogMods.RandomIsVisibleToLLM(f), } } @@ -679,6 +692,37 @@ func (m commsTextLogMods) RandomTwilioStatus(f *faker.Faker) CommsTextLogMod { }) } +// Set the model columns to this value +func (m commsTextLogMods) IsVisibleToLLM(val bool) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsVisibleToLLM = func() bool { return val } + }) +} + +// Set the Column from the function +func (m commsTextLogMods) IsVisibleToLLMFunc(f func() bool) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsVisibleToLLM = f + }) +} + +// Clear any values for the column +func (m commsTextLogMods) UnsetIsVisibleToLLM() CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsVisibleToLLM = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +func (m commsTextLogMods) RandomIsVisibleToLLM(f *faker.Faker) CommsTextLogMod { + return CommsTextLogModFunc(func(_ context.Context, o *CommsTextLogTemplate) { + o.IsVisibleToLLM = func() bool { + return random_bool(f) + } + }) +} + func (m commsTextLogMods) WithParentsCascading() CommsTextLogMod { return CommsTextLogModFunc(func(ctx context.Context, o *CommsTextLogTemplate) { if isDone, _ := commsTextLogWithParentsCascadingCtx.Value(ctx); isDone { diff --git a/db/factory/fieldseeker.containerrelate.bob.go b/db/factory/fieldseeker.containerrelate.bob.go index 31dbc2be..4cead88c 100644 --- a/db/factory/fieldseeker.containerrelate.bob.go +++ b/db/factory/fieldseeker.containerrelate.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerContainerrelateMod interface { diff --git a/db/factory/fieldseeker.fieldscoutinglog.bob.go b/db/factory/fieldseeker.fieldscoutinglog.bob.go index ac7f0351..ebc104ac 100644 --- a/db/factory/fieldseeker.fieldscoutinglog.bob.go +++ b/db/factory/fieldseeker.fieldscoutinglog.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerFieldscoutinglogMod interface { diff --git a/db/factory/fieldseeker.habitatrelate.bob.go b/db/factory/fieldseeker.habitatrelate.bob.go index 863ee087..7081f663 100644 --- a/db/factory/fieldseeker.habitatrelate.bob.go +++ b/db/factory/fieldseeker.habitatrelate.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerHabitatrelateMod interface { diff --git a/db/factory/fieldseeker.inspectionsample.bob.go b/db/factory/fieldseeker.inspectionsample.bob.go index 75993f79..a735a71d 100644 --- a/db/factory/fieldseeker.inspectionsample.bob.go +++ b/db/factory/fieldseeker.inspectionsample.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerInspectionsampleMod interface { diff --git a/db/factory/fieldseeker.inspectionsampledetail.bob.go b/db/factory/fieldseeker.inspectionsampledetail.bob.go index 53e60c10..827b0942 100644 --- a/db/factory/fieldseeker.inspectionsampledetail.bob.go +++ b/db/factory/fieldseeker.inspectionsampledetail.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerInspectionsampledetailMod interface { diff --git a/db/factory/fieldseeker.linelocation.bob.go b/db/factory/fieldseeker.linelocation.bob.go index 4cd24b90..cf5b71c9 100644 --- a/db/factory/fieldseeker.linelocation.bob.go +++ b/db/factory/fieldseeker.linelocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerLinelocationMod interface { diff --git a/db/factory/fieldseeker.locationtracking.bob.go b/db/factory/fieldseeker.locationtracking.bob.go index a258b8a1..5cafc232 100644 --- a/db/factory/fieldseeker.locationtracking.bob.go +++ b/db/factory/fieldseeker.locationtracking.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerLocationtrackingMod interface { diff --git a/db/factory/fieldseeker.mosquitoinspection.bob.go b/db/factory/fieldseeker.mosquitoinspection.bob.go index fa9c480a..3420f5d7 100644 --- a/db/factory/fieldseeker.mosquitoinspection.bob.go +++ b/db/factory/fieldseeker.mosquitoinspection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerMosquitoinspectionMod interface { diff --git a/db/factory/fieldseeker.pointlocation.bob.go b/db/factory/fieldseeker.pointlocation.bob.go index 01cf29b0..5eeb395f 100644 --- a/db/factory/fieldseeker.pointlocation.bob.go +++ b/db/factory/fieldseeker.pointlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerPointlocationMod interface { diff --git a/db/factory/fieldseeker.polygonlocation.bob.go b/db/factory/fieldseeker.polygonlocation.bob.go index 7def30d7..874b05ac 100644 --- a/db/factory/fieldseeker.polygonlocation.bob.go +++ b/db/factory/fieldseeker.polygonlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerPolygonlocationMod interface { diff --git a/db/factory/fieldseeker.pool.bob.go b/db/factory/fieldseeker.pool.bob.go index 322b8bcd..549bafcc 100644 --- a/db/factory/fieldseeker.pool.bob.go +++ b/db/factory/fieldseeker.pool.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerPoolMod interface { diff --git a/db/factory/fieldseeker.pooldetail.bob.go b/db/factory/fieldseeker.pooldetail.bob.go index c4978b51..624fe189 100644 --- a/db/factory/fieldseeker.pooldetail.bob.go +++ b/db/factory/fieldseeker.pooldetail.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerPooldetailMod interface { diff --git a/db/factory/fieldseeker.proposedtreatmentarea.bob.go b/db/factory/fieldseeker.proposedtreatmentarea.bob.go index a9dd58c6..05ce4a6c 100644 --- a/db/factory/fieldseeker.proposedtreatmentarea.bob.go +++ b/db/factory/fieldseeker.proposedtreatmentarea.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerProposedtreatmentareaMod interface { diff --git a/db/factory/fieldseeker.qamosquitoinspection.bob.go b/db/factory/fieldseeker.qamosquitoinspection.bob.go index cabb9b45..b99924d0 100644 --- a/db/factory/fieldseeker.qamosquitoinspection.bob.go +++ b/db/factory/fieldseeker.qamosquitoinspection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerQamosquitoinspectionMod interface { diff --git a/db/factory/fieldseeker.rodentlocation.bob.go b/db/factory/fieldseeker.rodentlocation.bob.go index 4911723e..b348a13e 100644 --- a/db/factory/fieldseeker.rodentlocation.bob.go +++ b/db/factory/fieldseeker.rodentlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerRodentlocationMod interface { diff --git a/db/factory/fieldseeker.samplecollection.bob.go b/db/factory/fieldseeker.samplecollection.bob.go index 29e18a7d..41890846 100644 --- a/db/factory/fieldseeker.samplecollection.bob.go +++ b/db/factory/fieldseeker.samplecollection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerSamplecollectionMod interface { diff --git a/db/factory/fieldseeker.samplelocation.bob.go b/db/factory/fieldseeker.samplelocation.bob.go index bc41d319..69a1a719 100644 --- a/db/factory/fieldseeker.samplelocation.bob.go +++ b/db/factory/fieldseeker.samplelocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerSamplelocationMod interface { diff --git a/db/factory/fieldseeker.servicerequest.bob.go b/db/factory/fieldseeker.servicerequest.bob.go index 4a69b438..2375df74 100644 --- a/db/factory/fieldseeker.servicerequest.bob.go +++ b/db/factory/fieldseeker.servicerequest.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerServicerequestMod interface { diff --git a/db/factory/fieldseeker.speciesabundance.bob.go b/db/factory/fieldseeker.speciesabundance.bob.go index efafd1fe..958d702d 100644 --- a/db/factory/fieldseeker.speciesabundance.bob.go +++ b/db/factory/fieldseeker.speciesabundance.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerSpeciesabundanceMod interface { diff --git a/db/factory/fieldseeker.stormdrain.bob.go b/db/factory/fieldseeker.stormdrain.bob.go index bb3fdca3..c3d5ac53 100644 --- a/db/factory/fieldseeker.stormdrain.bob.go +++ b/db/factory/fieldseeker.stormdrain.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerStormdrainMod interface { diff --git a/db/factory/fieldseeker.timecard.bob.go b/db/factory/fieldseeker.timecard.bob.go index fa18147b..f962dc43 100644 --- a/db/factory/fieldseeker.timecard.bob.go +++ b/db/factory/fieldseeker.timecard.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerTimecardMod interface { diff --git a/db/factory/fieldseeker.trapdata.bob.go b/db/factory/fieldseeker.trapdata.bob.go index 649b359f..12669f0c 100644 --- a/db/factory/fieldseeker.trapdata.bob.go +++ b/db/factory/fieldseeker.trapdata.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerTrapdatumMod interface { diff --git a/db/factory/fieldseeker.traplocation.bob.go b/db/factory/fieldseeker.traplocation.bob.go index 178079d8..6466750d 100644 --- a/db/factory/fieldseeker.traplocation.bob.go +++ b/db/factory/fieldseeker.traplocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerTraplocationMod interface { diff --git a/db/factory/fieldseeker.treatment.bob.go b/db/factory/fieldseeker.treatment.bob.go index dadf53bd..13949e67 100644 --- a/db/factory/fieldseeker.treatment.bob.go +++ b/db/factory/fieldseeker.treatment.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerTreatmentMod interface { diff --git a/db/factory/fieldseeker.treatmentarea.bob.go b/db/factory/fieldseeker.treatmentarea.bob.go index 2290a138..f8ec7fcb 100644 --- a/db/factory/fieldseeker.treatmentarea.bob.go +++ b/db/factory/fieldseeker.treatmentarea.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerTreatmentareaMod interface { diff --git a/db/factory/fieldseeker.zones.bob.go b/db/factory/fieldseeker.zones.bob.go index 1e6fab87..522c7721 100644 --- a/db/factory/fieldseeker.zones.bob.go +++ b/db/factory/fieldseeker.zones.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerZoneMod interface { diff --git a/db/factory/fieldseeker.zones2.bob.go b/db/factory/fieldseeker.zones2.bob.go index 9cf78520..04b70f03 100644 --- a/db/factory/fieldseeker.zones2.bob.go +++ b/db/factory/fieldseeker.zones2.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -9,14 +9,14 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/types" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/types" ) type FieldseekerZones2Mod interface { diff --git a/db/factory/fieldseeker_sync.bob.go b/db/factory/fieldseeker_sync.bob.go index 2358f34a..10736c41 100644 --- a/db/factory/fieldseeker_sync.bob.go +++ b/db/factory/fieldseeker_sync.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,10 +8,10 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type FieldseekerSyncMod interface { diff --git a/db/factory/geography_columns.bob.go b/db/factory/geography_columns.bob.go index 16709713..17469e4b 100644 --- a/db/factory/geography_columns.bob.go +++ b/db/factory/geography_columns.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory diff --git a/db/factory/geometry_columns.bob.go b/db/factory/geometry_columns.bob.go index 63c97d2a..20c97753 100644 --- a/db/factory/geometry_columns.bob.go +++ b/db/factory/geometry_columns.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory diff --git a/db/factory/goose_db_version.bob.go b/db/factory/goose_db_version.bob.go index 581502cb..01377c00 100644 --- a/db/factory/goose_db_version.bob.go +++ b/db/factory/goose_db_version.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,10 +8,10 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type GooseDBVersionMod interface { diff --git a/db/factory/h3_aggregation.bob.go b/db/factory/h3_aggregation.bob.go index 5977ac04..dc25bfa2 100644 --- a/db/factory/h3_aggregation.bob.go +++ b/db/factory/h3_aggregation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,13 +7,13 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type H3AggregationMod interface { diff --git a/db/factory/import.district.bob.go b/db/factory/import.district.bob.go index b2ebd11c..3cadd923 100644 --- a/db/factory/import.district.bob.go +++ b/db/factory/import.district.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,13 +7,13 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" "github.com/shopspring/decimal" - "github.com/stephenafamo/bob" ) type ImportDistrictMod interface { diff --git a/db/factory/note_audio.bob.go b/db/factory/note_audio.bob.go index 557894ba..5876c708 100644 --- a/db/factory/note_audio.bob.go +++ b/db/factory/note_audio.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type NoteAudioMod interface { diff --git a/db/factory/note_audio_breadcrumb.bob.go b/db/factory/note_audio_breadcrumb.bob.go index b4a8c8a2..b339b800 100644 --- a/db/factory/note_audio_breadcrumb.bob.go +++ b/db/factory/note_audio_breadcrumb.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,11 +8,11 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type NoteAudioBreadcrumbMod interface { diff --git a/db/factory/note_audio_data.bob.go b/db/factory/note_audio_data.bob.go index 3030278b..70983493 100644 --- a/db/factory/note_audio_data.bob.go +++ b/db/factory/note_audio_data.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,12 +8,12 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type NoteAudioDatumMod interface { diff --git a/db/factory/note_image.bob.go b/db/factory/note_image.bob.go index 53cc85cf..db3cecb7 100644 --- a/db/factory/note_image.bob.go +++ b/db/factory/note_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type NoteImageMod interface { diff --git a/db/factory/note_image_breadcrumb.bob.go b/db/factory/note_image_breadcrumb.bob.go index 31cb44f6..b51fe9f9 100644 --- a/db/factory/note_image_breadcrumb.bob.go +++ b/db/factory/note_image_breadcrumb.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,11 +8,11 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type NoteImageBreadcrumbMod interface { diff --git a/db/factory/note_image_data.bob.go b/db/factory/note_image_data.bob.go index dc8350ce..caf0b1e9 100644 --- a/db/factory/note_image_data.bob.go +++ b/db/factory/note_image_data.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,12 +8,12 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type NoteImageDatumMod interface { diff --git a/db/factory/notification.bob.go b/db/factory/notification.bob.go index 9b34b0c7..874649ec 100644 --- a/db/factory/notification.bob.go +++ b/db/factory/notification.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type NotificationMod interface { diff --git a/db/factory/oauth_token.bob.go b/db/factory/oauth_token.bob.go index 12ef5b67..8bb6fe31 100644 --- a/db/factory/oauth_token.bob.go +++ b/db/factory/oauth_token.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,12 +8,12 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type OauthTokenMod interface { diff --git a/db/factory/organization.bob.go b/db/factory/organization.bob.go index e92ea5a7..e5396b91 100644 --- a/db/factory/organization.bob.go +++ b/db/factory/organization.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,13 +7,13 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type OrganizationMod interface { diff --git a/db/factory/publicreport.image.bob.go b/db/factory/publicreport.image.bob.go index cf3044aa..7772d7a9 100644 --- a/db/factory/publicreport.image.bob.go +++ b/db/factory/publicreport.image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type PublicreportImageMod interface { diff --git a/db/factory/publicreport.image_exif.bob.go b/db/factory/publicreport.image_exif.bob.go index 742829a4..1726c82a 100644 --- a/db/factory/publicreport.image_exif.bob.go +++ b/db/factory/publicreport.image_exif.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,10 +7,10 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type PublicreportImageExifMod interface { diff --git a/db/factory/publicreport.nuisance.bob.go b/db/factory/publicreport.nuisance.bob.go index faccd615..aa9b49de 100644 --- a/db/factory/publicreport.nuisance.bob.go +++ b/db/factory/publicreport.nuisance.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type PublicreportNuisanceMod interface { diff --git a/db/factory/publicreport.pool.bob.go b/db/factory/publicreport.pool.bob.go index 103060a5..34bac1f9 100644 --- a/db/factory/publicreport.pool.bob.go +++ b/db/factory/publicreport.pool.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type PublicreportPoolMod interface { diff --git a/db/factory/publicreport.pool_image.bob.go b/db/factory/publicreport.pool_image.bob.go index e7af585c..ce98ed9a 100644 --- a/db/factory/publicreport.pool_image.bob.go +++ b/db/factory/publicreport.pool_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,10 +7,10 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type PublicreportPoolImageMod interface { diff --git a/db/factory/publicreport.quick.bob.go b/db/factory/publicreport.quick.bob.go index 30ff2b16..97cec339 100644 --- a/db/factory/publicreport.quick.bob.go +++ b/db/factory/publicreport.quick.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type PublicreportQuickMod interface { diff --git a/db/factory/publicreport.quick_image.bob.go b/db/factory/publicreport.quick_image.bob.go index 951dc333..63873cbf 100644 --- a/db/factory/publicreport.quick_image.bob.go +++ b/db/factory/publicreport.quick_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,10 +7,10 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type PublicreportQuickImageMod interface { diff --git a/db/factory/publicreport.report_location.bob.go b/db/factory/publicreport.report_location.bob.go index 2b1dac21..44b0057a 100644 --- a/db/factory/publicreport.report_location.bob.go +++ b/db/factory/publicreport.report_location.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory diff --git a/db/factory/raster_columns.bob.go b/db/factory/raster_columns.bob.go index bd01e09e..1e8a8255 100644 --- a/db/factory/raster_columns.bob.go +++ b/db/factory/raster_columns.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory diff --git a/db/factory/raster_overviews.bob.go b/db/factory/raster_overviews.bob.go index 3d7f41da..7c4bb9bf 100644 --- a/db/factory/raster_overviews.bob.go +++ b/db/factory/raster_overviews.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory diff --git a/db/factory/sessions.bob.go b/db/factory/sessions.bob.go index d0148f28..562aed8a 100644 --- a/db/factory/sessions.bob.go +++ b/db/factory/sessions.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,10 +8,10 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type SessionMod interface { diff --git a/db/factory/spatial_ref_sys.bob.go b/db/factory/spatial_ref_sys.bob.go index 8eeff8fa..f59a43da 100644 --- a/db/factory/spatial_ref_sys.bob.go +++ b/db/factory/spatial_ref_sys.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -7,12 +7,12 @@ import ( "context" "testing" + "github.com/Gleipnir-Technology/bob" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type SpatialRefSyMod interface { diff --git a/db/factory/user_.bob.go b/db/factory/user_.bob.go index 0bfd18f8..5112ee5b 100644 --- a/db/factory/user_.bob.go +++ b/db/factory/user_.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package factory @@ -8,13 +8,13 @@ import ( "testing" "time" + "github.com/Gleipnir-Technology/bob" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" models "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/jaswdr/faker/v2" - "github.com/stephenafamo/bob" ) type UserMod interface { diff --git a/db/fieldseeker.go b/db/fieldseeker.go index 24d80942..ecc4f4e3 100644 --- a/db/fieldseeker.go +++ b/db/fieldseeker.go @@ -5,14 +5,14 @@ import ( "fmt" fslayer "github.com/Gleipnir-Technology/arcgis-go/fieldseeker/layer" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/gofrs/uuid/v5" googleuuid "github.com/google/uuid" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" "github.com/stephenafamo/scan" ) diff --git a/db/migrations/00044_comms_text_origin_user.sql b/db/migrations/00044_comms_text_origin_user.sql index f8aa6829..2e75d3fa 100644 --- a/db/migrations/00044_comms_text_origin_user.sql +++ b/db/migrations/00044_comms_text_origin_user.sql @@ -1,6 +1,3 @@ -- +goose Up ALTER TYPE comms.TextOrigin ADD VALUE 'customer'; ALTER TYPE comms.TextOrigin ADD VALUE 'reiteration'; --- +goose Down -ALTER TYPE comms.TextOrigin DROP VALUE 'reiteration'; -ALTER TYPE comms.TextOrigin DROP VALUE 'customer'; diff --git a/db/migrations/00045_comms_text_sid.sql b/db/migrations/00045_comms_text_sid.sql index 71b0e09e..bbda4771 100644 --- a/db/migrations/00045_comms_text_sid.sql +++ b/db/migrations/00045_comms_text_sid.sql @@ -3,6 +3,11 @@ ALTER TABLE comms.text_log ADD COLUMN twilio_sid TEXT UNIQUE; ALTER TABLE comms.text_log ADD COLUMN twilio_status TEXT; UPDATE comms.text_log SET twilio_status = ''; ALTER TABLE comms.text_log ALTER COLUMN twilio_status SET NOT NULL; +ALTER TABLE comms.text_log ADD COLUMN is_visible_to_llm BOOLEAN; +UPDATE comms.text_log SET is_visible_to_llm = FALSE; +ALTER TABLE comms.text_log ALTER COLUMN is_visible_to_llm SET NOT NULL; +ALTER TYPE comms.TextOrigin ADD VALUE 'command-response'; -- +goose Down +ALTER TABLE comms.text_log DROP COLUMN is_visible_to_llm; ALTER TABLE comms.text_log DROP COLUMN twilio_status; ALTER TABLE comms.text_log DROP COLUMN twilio_sid; diff --git a/db/models/arcgis.user_.bob.go b/db/models/arcgis.user_.bob.go index 6613b87f..3796abf0 100644 --- a/db/models/arcgis.user_.bob.go +++ b/db/models/arcgis.user_.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,17 +9,17 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // ArcgisUser is an object representing the database table. diff --git a/db/models/arcgis.user_privilege.bob.go b/db/models/arcgis.user_privilege.bob.go index 0783119b..00b0a456 100644 --- a/db/models/arcgis.user_privilege.bob.go +++ b/db/models/arcgis.user_privilege.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,17 +8,17 @@ import ( "fmt" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // ArcgisUserPrivilege is an object representing the database table. diff --git a/db/models/bob_counts.bob.go b/db/models/bob_counts.bob.go index 41648f3a..219db03a 100644 --- a/db/models/bob_counts.bob.go +++ b/db/models/bob_counts.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -7,10 +7,10 @@ import ( "context" "io" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/orm" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/orm" "github.com/stephenafamo/scan" ) diff --git a/db/models/bob_joins.bob.go b/db/models/bob_joins.bob.go index a69510f4..e6c2439d 100644 --- a/db/models/bob_joins.bob.go +++ b/db/models/bob_joins.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -6,9 +6,9 @@ package models import ( "hash/maphash" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/clause" - "github.com/stephenafamo/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/clause" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" ) var ( diff --git a/db/models/bob_loaders.bob.go b/db/models/bob_loaders.bob.go index 3a003482..708a4e90 100644 --- a/db/models/bob_loaders.bob.go +++ b/db/models/bob_loaders.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,9 +9,9 @@ import ( "errors" "fmt" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/orm" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/orm" ) var Preload = getPreloaders() diff --git a/db/models/bob_where.bob.go b/db/models/bob_where.bob.go index d25228a0..2eaec9d9 100644 --- a/db/models/bob_where.bob.go +++ b/db/models/bob_where.bob.go @@ -1,12 +1,12 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models import ( - "github.com/stephenafamo/bob/clause" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/clause" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" ) var ( diff --git a/db/models/comms.email_contact.bob.go b/db/models/comms.email_contact.bob.go index ca468f6b..ff97fc8d 100644 --- a/db/models/comms.email_contact.bob.go +++ b/db/models/comms.email_contact.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,17 +8,17 @@ import ( "fmt" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // CommsEmailContact is an object representing the database table. diff --git a/db/models/comms.email_log.bob.go b/db/models/comms.email_log.bob.go index ef59ff78..d22a9269 100644 --- a/db/models/comms.email_log.bob.go +++ b/db/models/comms.email_log.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,20 +9,20 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // CommsEmailLog is an object representing the database table. diff --git a/db/models/comms.email_template.bob.go b/db/models/comms.email_template.bob.go index ca95017d..0f62168d 100644 --- a/db/models/comms.email_template.bob.go +++ b/db/models/comms.email_template.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,20 +9,20 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // CommsEmailTemplate is an object representing the database table. diff --git a/db/models/comms.phone.bob.go b/db/models/comms.phone.bob.go index 56a665c0..70bc8a63 100644 --- a/db/models/comms.phone.bob.go +++ b/db/models/comms.phone.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,19 +8,19 @@ import ( "fmt" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // CommsPhone is an object representing the database table. diff --git a/db/models/comms.text_job.bob.go b/db/models/comms.text_job.bob.go index 6a8ce5b5..aabb5a6a 100644 --- a/db/models/comms.text_job.bob.go +++ b/db/models/comms.text_job.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,18 +9,18 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // CommsTextJob is an object representing the database table. diff --git a/db/models/comms.text_log.bob.go b/db/models/comms.text_log.bob.go index 45e671d0..33c31b19 100644 --- a/db/models/comms.text_log.bob.go +++ b/db/models/comms.text_log.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,33 +9,34 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // CommsTextLog is an object representing the database table. type CommsTextLog struct { - Content string `db:"content" ` - Created time.Time `db:"created" ` - Destination string `db:"destination" ` - ID int32 `db:"id,pk" ` - IsWelcome bool `db:"is_welcome" ` - Origin enums.CommsTextorigin `db:"origin" ` - Source string `db:"source" ` - TwilioSid null.Val[string] `db:"twilio_sid" ` - TwilioStatus string `db:"twilio_status" ` + Content string `db:"content" ` + Created time.Time `db:"created" ` + Destination string `db:"destination" ` + ID int32 `db:"id,pk" ` + IsWelcome bool `db:"is_welcome" ` + Origin enums.CommsTextorigin `db:"origin" ` + Source string `db:"source" ` + TwilioSid null.Val[string] `db:"twilio_sid" ` + TwilioStatus string `db:"twilio_status" ` + IsVisibleToLLM bool `db:"is_visible_to_llm" ` R commsTextLogR `db:"-" ` } @@ -59,33 +60,35 @@ type commsTextLogR struct { func buildCommsTextLogColumns(alias string) commsTextLogColumns { return commsTextLogColumns{ ColumnsExpr: expr.NewColumnsExpr( - "content", "created", "destination", "id", "is_welcome", "origin", "source", "twilio_sid", "twilio_status", + "content", "created", "destination", "id", "is_welcome", "origin", "source", "twilio_sid", "twilio_status", "is_visible_to_llm", ).WithParent("comms.text_log"), - tableAlias: alias, - Content: psql.Quote(alias, "content"), - Created: psql.Quote(alias, "created"), - Destination: psql.Quote(alias, "destination"), - ID: psql.Quote(alias, "id"), - IsWelcome: psql.Quote(alias, "is_welcome"), - Origin: psql.Quote(alias, "origin"), - Source: psql.Quote(alias, "source"), - TwilioSid: psql.Quote(alias, "twilio_sid"), - TwilioStatus: psql.Quote(alias, "twilio_status"), + tableAlias: alias, + Content: psql.Quote(alias, "content"), + Created: psql.Quote(alias, "created"), + Destination: psql.Quote(alias, "destination"), + ID: psql.Quote(alias, "id"), + IsWelcome: psql.Quote(alias, "is_welcome"), + Origin: psql.Quote(alias, "origin"), + Source: psql.Quote(alias, "source"), + TwilioSid: psql.Quote(alias, "twilio_sid"), + TwilioStatus: psql.Quote(alias, "twilio_status"), + IsVisibleToLLM: psql.Quote(alias, "is_visible_to_llm"), } } type commsTextLogColumns struct { expr.ColumnsExpr - tableAlias string - Content psql.Expression - Created psql.Expression - Destination psql.Expression - ID psql.Expression - IsWelcome psql.Expression - Origin psql.Expression - Source psql.Expression - TwilioSid psql.Expression - TwilioStatus psql.Expression + tableAlias string + Content psql.Expression + Created psql.Expression + Destination psql.Expression + ID psql.Expression + IsWelcome psql.Expression + Origin psql.Expression + Source psql.Expression + TwilioSid psql.Expression + TwilioStatus psql.Expression + IsVisibleToLLM psql.Expression } func (c commsTextLogColumns) Alias() string { @@ -100,19 +103,20 @@ func (commsTextLogColumns) AliasedAs(alias string) commsTextLogColumns { // All values are optional, and do not have to be set // Generated columns are not included type CommsTextLogSetter struct { - Content omit.Val[string] `db:"content" ` - Created omit.Val[time.Time] `db:"created" ` - Destination omit.Val[string] `db:"destination" ` - ID omit.Val[int32] `db:"id,pk" ` - IsWelcome omit.Val[bool] `db:"is_welcome" ` - Origin omit.Val[enums.CommsTextorigin] `db:"origin" ` - Source omit.Val[string] `db:"source" ` - TwilioSid omitnull.Val[string] `db:"twilio_sid" ` - TwilioStatus omit.Val[string] `db:"twilio_status" ` + Content omit.Val[string] `db:"content" ` + Created omit.Val[time.Time] `db:"created" ` + Destination omit.Val[string] `db:"destination" ` + ID omit.Val[int32] `db:"id,pk" ` + IsWelcome omit.Val[bool] `db:"is_welcome" ` + Origin omit.Val[enums.CommsTextorigin] `db:"origin" ` + Source omit.Val[string] `db:"source" ` + TwilioSid omitnull.Val[string] `db:"twilio_sid" ` + TwilioStatus omit.Val[string] `db:"twilio_status" ` + IsVisibleToLLM omit.Val[bool] `db:"is_visible_to_llm" ` } func (s CommsTextLogSetter) SetColumns() []string { - vals := make([]string, 0, 9) + vals := make([]string, 0, 10) if s.Content.IsValue() { vals = append(vals, "content") } @@ -140,6 +144,9 @@ func (s CommsTextLogSetter) SetColumns() []string { if s.TwilioStatus.IsValue() { vals = append(vals, "twilio_status") } + if s.IsVisibleToLLM.IsValue() { + vals = append(vals, "is_visible_to_llm") + } return vals } @@ -171,6 +178,9 @@ func (s CommsTextLogSetter) Overwrite(t *CommsTextLog) { if s.TwilioStatus.IsValue() { t.TwilioStatus = s.TwilioStatus.MustGet() } + if s.IsVisibleToLLM.IsValue() { + t.IsVisibleToLLM = s.IsVisibleToLLM.MustGet() + } } func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { @@ -179,7 +189,7 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { }) q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.StringWriter, d bob.Dialect, start int) ([]any, error) { - vals := make([]bob.Expression, 9) + vals := make([]bob.Expression, 10) if s.Content.IsValue() { vals[0] = psql.Arg(s.Content.MustGet()) } else { @@ -234,6 +244,12 @@ func (s *CommsTextLogSetter) Apply(q *dialect.InsertQuery) { vals[8] = psql.Raw("DEFAULT") } + if s.IsVisibleToLLM.IsValue() { + vals[9] = psql.Arg(s.IsVisibleToLLM.MustGet()) + } else { + vals[9] = psql.Raw("DEFAULT") + } + return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "") })) } @@ -243,7 +259,7 @@ func (s CommsTextLogSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] { } func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { - exprs := make([]bob.Expression, 0, 9) + exprs := make([]bob.Expression, 0, 10) if s.Content.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ @@ -308,6 +324,13 @@ func (s CommsTextLogSetter) Expressions(prefix ...string) []bob.Expression { }}) } + if s.IsVisibleToLLM.IsValue() { + exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ + psql.Quote(append(prefix, "is_visible_to_llm")...), + psql.Arg(s.IsVisibleToLLM), + }}) + } + return exprs } @@ -679,15 +702,16 @@ func (commsTextLog0 *CommsTextLog) AttachSourcePhone(ctx context.Context, exec b } type commsTextLogWhere[Q psql.Filterable] struct { - Content psql.WhereMod[Q, string] - Created psql.WhereMod[Q, time.Time] - Destination psql.WhereMod[Q, string] - ID psql.WhereMod[Q, int32] - IsWelcome psql.WhereMod[Q, bool] - Origin psql.WhereMod[Q, enums.CommsTextorigin] - Source psql.WhereMod[Q, string] - TwilioSid psql.WhereNullMod[Q, string] - TwilioStatus psql.WhereMod[Q, string] + Content psql.WhereMod[Q, string] + Created psql.WhereMod[Q, time.Time] + Destination psql.WhereMod[Q, string] + ID psql.WhereMod[Q, int32] + IsWelcome psql.WhereMod[Q, bool] + Origin psql.WhereMod[Q, enums.CommsTextorigin] + Source psql.WhereMod[Q, string] + TwilioSid psql.WhereNullMod[Q, string] + TwilioStatus psql.WhereMod[Q, string] + IsVisibleToLLM psql.WhereMod[Q, bool] } func (commsTextLogWhere[Q]) AliasedAs(alias string) commsTextLogWhere[Q] { @@ -696,15 +720,16 @@ func (commsTextLogWhere[Q]) AliasedAs(alias string) commsTextLogWhere[Q] { func buildCommsTextLogWhere[Q psql.Filterable](cols commsTextLogColumns) commsTextLogWhere[Q] { return commsTextLogWhere[Q]{ - Content: psql.Where[Q, string](cols.Content), - Created: psql.Where[Q, time.Time](cols.Created), - Destination: psql.Where[Q, string](cols.Destination), - ID: psql.Where[Q, int32](cols.ID), - IsWelcome: psql.Where[Q, bool](cols.IsWelcome), - Origin: psql.Where[Q, enums.CommsTextorigin](cols.Origin), - Source: psql.Where[Q, string](cols.Source), - TwilioSid: psql.WhereNull[Q, string](cols.TwilioSid), - TwilioStatus: psql.Where[Q, string](cols.TwilioStatus), + Content: psql.Where[Q, string](cols.Content), + Created: psql.Where[Q, time.Time](cols.Created), + Destination: psql.Where[Q, string](cols.Destination), + ID: psql.Where[Q, int32](cols.ID), + IsWelcome: psql.Where[Q, bool](cols.IsWelcome), + Origin: psql.Where[Q, enums.CommsTextorigin](cols.Origin), + Source: psql.Where[Q, string](cols.Source), + TwilioSid: psql.WhereNull[Q, string](cols.TwilioSid), + TwilioStatus: psql.Where[Q, string](cols.TwilioStatus), + IsVisibleToLLM: psql.Where[Q, bool](cols.IsVisibleToLLM), } } diff --git a/db/models/fieldseeker.containerrelate.bob.go b/db/models/fieldseeker.containerrelate.bob.go index 8b439dce..3d08edbd 100644 --- a/db/models/fieldseeker.containerrelate.bob.go +++ b/db/models/fieldseeker.containerrelate.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerContainerrelate is an object representing the database table. diff --git a/db/models/fieldseeker.fieldscoutinglog.bob.go b/db/models/fieldseeker.fieldscoutinglog.bob.go index a0caa392..ff221e64 100644 --- a/db/models/fieldseeker.fieldscoutinglog.bob.go +++ b/db/models/fieldseeker.fieldscoutinglog.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerFieldscoutinglog is an object representing the database table. diff --git a/db/models/fieldseeker.habitatrelate.bob.go b/db/models/fieldseeker.habitatrelate.bob.go index cefb73a8..37743713 100644 --- a/db/models/fieldseeker.habitatrelate.bob.go +++ b/db/models/fieldseeker.habitatrelate.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerHabitatrelate is an object representing the database table. diff --git a/db/models/fieldseeker.inspectionsample.bob.go b/db/models/fieldseeker.inspectionsample.bob.go index 73aaa6da..b70b3452 100644 --- a/db/models/fieldseeker.inspectionsample.bob.go +++ b/db/models/fieldseeker.inspectionsample.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerInspectionsample is an object representing the database table. diff --git a/db/models/fieldseeker.inspectionsampledetail.bob.go b/db/models/fieldseeker.inspectionsampledetail.bob.go index d9d4d406..fde2deb9 100644 --- a/db/models/fieldseeker.inspectionsampledetail.bob.go +++ b/db/models/fieldseeker.inspectionsampledetail.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerInspectionsampledetail is an object representing the database table. diff --git a/db/models/fieldseeker.linelocation.bob.go b/db/models/fieldseeker.linelocation.bob.go index 9efc4ee5..dfa31d27 100644 --- a/db/models/fieldseeker.linelocation.bob.go +++ b/db/models/fieldseeker.linelocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerLinelocation is an object representing the database table. diff --git a/db/models/fieldseeker.locationtracking.bob.go b/db/models/fieldseeker.locationtracking.bob.go index facfcf7d..452a6c61 100644 --- a/db/models/fieldseeker.locationtracking.bob.go +++ b/db/models/fieldseeker.locationtracking.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerLocationtracking is an object representing the database table. diff --git a/db/models/fieldseeker.mosquitoinspection.bob.go b/db/models/fieldseeker.mosquitoinspection.bob.go index db094235..89e5d974 100644 --- a/db/models/fieldseeker.mosquitoinspection.bob.go +++ b/db/models/fieldseeker.mosquitoinspection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerMosquitoinspection is an object representing the database table. diff --git a/db/models/fieldseeker.pointlocation.bob.go b/db/models/fieldseeker.pointlocation.bob.go index e212b558..0b4b92e8 100644 --- a/db/models/fieldseeker.pointlocation.bob.go +++ b/db/models/fieldseeker.pointlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerPointlocation is an object representing the database table. diff --git a/db/models/fieldseeker.polygonlocation.bob.go b/db/models/fieldseeker.polygonlocation.bob.go index 5003caa2..69b142ce 100644 --- a/db/models/fieldseeker.polygonlocation.bob.go +++ b/db/models/fieldseeker.polygonlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerPolygonlocation is an object representing the database table. diff --git a/db/models/fieldseeker.pool.bob.go b/db/models/fieldseeker.pool.bob.go index 898e7bcb..ea2129b0 100644 --- a/db/models/fieldseeker.pool.bob.go +++ b/db/models/fieldseeker.pool.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerPool is an object representing the database table. diff --git a/db/models/fieldseeker.pooldetail.bob.go b/db/models/fieldseeker.pooldetail.bob.go index a9cbd612..38b65824 100644 --- a/db/models/fieldseeker.pooldetail.bob.go +++ b/db/models/fieldseeker.pooldetail.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerPooldetail is an object representing the database table. diff --git a/db/models/fieldseeker.proposedtreatmentarea.bob.go b/db/models/fieldseeker.proposedtreatmentarea.bob.go index 8f89fe65..413a4ebd 100644 --- a/db/models/fieldseeker.proposedtreatmentarea.bob.go +++ b/db/models/fieldseeker.proposedtreatmentarea.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerProposedtreatmentarea is an object representing the database table. diff --git a/db/models/fieldseeker.qamosquitoinspection.bob.go b/db/models/fieldseeker.qamosquitoinspection.bob.go index 76fcae96..2f73acba 100644 --- a/db/models/fieldseeker.qamosquitoinspection.bob.go +++ b/db/models/fieldseeker.qamosquitoinspection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerQamosquitoinspection is an object representing the database table. diff --git a/db/models/fieldseeker.rodentlocation.bob.go b/db/models/fieldseeker.rodentlocation.bob.go index 28117cfb..de21bf63 100644 --- a/db/models/fieldseeker.rodentlocation.bob.go +++ b/db/models/fieldseeker.rodentlocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerRodentlocation is an object representing the database table. diff --git a/db/models/fieldseeker.samplecollection.bob.go b/db/models/fieldseeker.samplecollection.bob.go index f3648eda..d4bbbb9f 100644 --- a/db/models/fieldseeker.samplecollection.bob.go +++ b/db/models/fieldseeker.samplecollection.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerSamplecollection is an object representing the database table. diff --git a/db/models/fieldseeker.samplelocation.bob.go b/db/models/fieldseeker.samplelocation.bob.go index cde38c45..ffa55a24 100644 --- a/db/models/fieldseeker.samplelocation.bob.go +++ b/db/models/fieldseeker.samplelocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerSamplelocation is an object representing the database table. diff --git a/db/models/fieldseeker.servicerequest.bob.go b/db/models/fieldseeker.servicerequest.bob.go index f3b89f50..7f736675 100644 --- a/db/models/fieldseeker.servicerequest.bob.go +++ b/db/models/fieldseeker.servicerequest.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerServicerequest is an object representing the database table. diff --git a/db/models/fieldseeker.speciesabundance.bob.go b/db/models/fieldseeker.speciesabundance.bob.go index f8b46124..894630a0 100644 --- a/db/models/fieldseeker.speciesabundance.bob.go +++ b/db/models/fieldseeker.speciesabundance.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerSpeciesabundance is an object representing the database table. diff --git a/db/models/fieldseeker.stormdrain.bob.go b/db/models/fieldseeker.stormdrain.bob.go index c603b20c..c67451a9 100644 --- a/db/models/fieldseeker.stormdrain.bob.go +++ b/db/models/fieldseeker.stormdrain.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerStormdrain is an object representing the database table. diff --git a/db/models/fieldseeker.timecard.bob.go b/db/models/fieldseeker.timecard.bob.go index 8b7b1805..5dfed24a 100644 --- a/db/models/fieldseeker.timecard.bob.go +++ b/db/models/fieldseeker.timecard.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerTimecard is an object representing the database table. diff --git a/db/models/fieldseeker.trapdata.bob.go b/db/models/fieldseeker.trapdata.bob.go index f6fe3f01..8075cd5b 100644 --- a/db/models/fieldseeker.trapdata.bob.go +++ b/db/models/fieldseeker.trapdata.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerTrapdatum is an object representing the database table. diff --git a/db/models/fieldseeker.traplocation.bob.go b/db/models/fieldseeker.traplocation.bob.go index ba711df6..e6c0a5b9 100644 --- a/db/models/fieldseeker.traplocation.bob.go +++ b/db/models/fieldseeker.traplocation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerTraplocation is an object representing the database table. diff --git a/db/models/fieldseeker.treatment.bob.go b/db/models/fieldseeker.treatment.bob.go index d219c571..81f67bbd 100644 --- a/db/models/fieldseeker.treatment.bob.go +++ b/db/models/fieldseeker.treatment.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerTreatment is an object representing the database table. diff --git a/db/models/fieldseeker.treatmentarea.bob.go b/db/models/fieldseeker.treatmentarea.bob.go index 6e1ffcb8..46c42d5a 100644 --- a/db/models/fieldseeker.treatmentarea.bob.go +++ b/db/models/fieldseeker.treatmentarea.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerTreatmentarea is an object representing the database table. diff --git a/db/models/fieldseeker.zones.bob.go b/db/models/fieldseeker.zones.bob.go index e5e9f7b4..8d2cabad 100644 --- a/db/models/fieldseeker.zones.bob.go +++ b/db/models/fieldseeker.zones.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerZone is an object representing the database table. diff --git a/db/models/fieldseeker.zones2.bob.go b/db/models/fieldseeker.zones2.bob.go index 02e3a743..18b69253 100644 --- a/db/models/fieldseeker.zones2.bob.go +++ b/db/models/fieldseeker.zones2.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,21 +10,21 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerZones2 is an object representing the database table. diff --git a/db/models/fieldseeker_sync.bob.go b/db/models/fieldseeker_sync.bob.go index fcf5e738..f1cf9e73 100644 --- a/db/models/fieldseeker_sync.bob.go +++ b/db/models/fieldseeker_sync.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,17 +9,17 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // FieldseekerSync is an object representing the database table. diff --git a/db/models/geography_columns.bob.go b/db/models/geography_columns.bob.go index c1452873..59a4edd7 100644 --- a/db/models/geography_columns.bob.go +++ b/db/models/geography_columns.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -6,10 +6,10 @@ package models import ( "context" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/expr" "github.com/aarondl/opt/null" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/expr" ) // GeographyColumn is an object representing the database table. diff --git a/db/models/geometry_columns.bob.go b/db/models/geometry_columns.bob.go index 5d875b78..1d9f9f6a 100644 --- a/db/models/geometry_columns.bob.go +++ b/db/models/geometry_columns.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -6,10 +6,10 @@ package models import ( "context" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/expr" "github.com/aarondl/opt/null" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/expr" ) // GeometryColumn is an object representing the database table. diff --git a/db/models/goose_db_version.bob.go b/db/models/goose_db_version.bob.go index 19519e27..b68acace 100644 --- a/db/models/goose_db_version.bob.go +++ b/db/models/goose_db_version.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,14 +8,14 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" ) // GooseDBVersion is an object representing the database table. diff --git a/db/models/h3_aggregation.bob.go b/db/models/h3_aggregation.bob.go index 7e09d5c5..1de3e34a 100644 --- a/db/models/h3_aggregation.bob.go +++ b/db/models/h3_aggregation.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,20 +8,20 @@ import ( "fmt" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // H3Aggregation is an object representing the database table. diff --git a/db/models/import.district.bob.go b/db/models/import.district.bob.go index 6f922256..6523d151 100644 --- a/db/models/import.district.bob.go +++ b/db/models/import.district.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,20 +8,20 @@ import ( "fmt" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/shopspring/decimal" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // ImportDistrict is an object representing the database table. diff --git a/db/models/note_audio.bob.go b/db/models/note_audio.bob.go index 204cb229..66a9e2a6 100644 --- a/db/models/note_audio.bob.go +++ b/db/models/note_audio.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,20 +9,20 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // NoteAudio is an object representing the database table. diff --git a/db/models/note_audio_breadcrumb.bob.go b/db/models/note_audio_breadcrumb.bob.go index 67165c53..c25e40b8 100644 --- a/db/models/note_audio_breadcrumb.bob.go +++ b/db/models/note_audio_breadcrumb.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,18 +9,18 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/omit" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // NoteAudioBreadcrumb is an object representing the database table. diff --git a/db/models/note_audio_data.bob.go b/db/models/note_audio_data.bob.go index 89dd6055..d1304e38 100644 --- a/db/models/note_audio_data.bob.go +++ b/db/models/note_audio_data.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,19 +9,19 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/omit" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // NoteAudioDatum is an object representing the database table. diff --git a/db/models/note_image.bob.go b/db/models/note_image.bob.go index 34e4edd2..2185c6e9 100644 --- a/db/models/note_image.bob.go +++ b/db/models/note_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,20 +9,20 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // NoteImage is an object representing the database table. diff --git a/db/models/note_image_breadcrumb.bob.go b/db/models/note_image_breadcrumb.bob.go index be232187..2cfc2f79 100644 --- a/db/models/note_image_breadcrumb.bob.go +++ b/db/models/note_image_breadcrumb.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,18 +9,18 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/omit" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // NoteImageBreadcrumb is an object representing the database table. diff --git a/db/models/note_image_data.bob.go b/db/models/note_image_data.bob.go index 2eb3f729..8bac0d0a 100644 --- a/db/models/note_image_data.bob.go +++ b/db/models/note_image_data.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,19 +9,19 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/omit" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // NoteImageDatum is an object representing the database table. diff --git a/db/models/notification.bob.go b/db/models/notification.bob.go index ee150774..b2166395 100644 --- a/db/models/notification.bob.go +++ b/db/models/notification.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,20 +9,20 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // Notification is an object representing the database table. diff --git a/db/models/oauth_token.bob.go b/db/models/oauth_token.bob.go index 9b8c7156..8d8efe52 100644 --- a/db/models/oauth_token.bob.go +++ b/db/models/oauth_token.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,19 +9,19 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // OauthToken is an object representing the database table. diff --git a/db/models/organization.bob.go b/db/models/organization.bob.go index 8876432c..40ce86eb 100644 --- a/db/models/organization.bob.go +++ b/db/models/organization.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,20 +8,20 @@ import ( "fmt" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // Organization is an object representing the database table. diff --git a/db/models/publicreport.image.bob.go b/db/models/publicreport.image.bob.go index 0dc68ab3..c0601c2b 100644 --- a/db/models/publicreport.image.bob.go +++ b/db/models/publicreport.image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,20 +10,20 @@ import ( "strconv" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" "github.com/stephenafamo/scan" ) diff --git a/db/models/publicreport.image_exif.bob.go b/db/models/publicreport.image_exif.bob.go index bc4ac461..d7050087 100644 --- a/db/models/publicreport.image_exif.bob.go +++ b/db/models/publicreport.image_exif.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,17 +8,17 @@ import ( "fmt" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // PublicreportImageExif is an object representing the database table. diff --git a/db/models/publicreport.nuisance.bob.go b/db/models/publicreport.nuisance.bob.go index 0797594d..ad16bd4f 100644 --- a/db/models/publicreport.nuisance.bob.go +++ b/db/models/publicreport.nuisance.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,20 +9,20 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // PublicreportNuisance is an object representing the database table. diff --git a/db/models/publicreport.pool.bob.go b/db/models/publicreport.pool.bob.go index 3b5f7c08..a43fbcf9 100644 --- a/db/models/publicreport.pool.bob.go +++ b/db/models/publicreport.pool.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,20 +10,20 @@ import ( "strconv" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" "github.com/stephenafamo/scan" ) diff --git a/db/models/publicreport.pool_image.bob.go b/db/models/publicreport.pool_image.bob.go index 3547232d..289ae655 100644 --- a/db/models/publicreport.pool_image.bob.go +++ b/db/models/publicreport.pool_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,17 +8,17 @@ import ( "fmt" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // PublicreportPoolImage is an object representing the database table. diff --git a/db/models/publicreport.quick.bob.go b/db/models/publicreport.quick.bob.go index 0486aebb..3e4ae67d 100644 --- a/db/models/publicreport.quick.bob.go +++ b/db/models/publicreport.quick.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -10,20 +10,20 @@ import ( "strconv" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" "github.com/stephenafamo/scan" ) diff --git a/db/models/publicreport.quick_image.bob.go b/db/models/publicreport.quick_image.bob.go index 9d78ab8c..5bf0c644 100644 --- a/db/models/publicreport.quick_image.bob.go +++ b/db/models/publicreport.quick_image.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,17 +8,17 @@ import ( "fmt" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // PublicreportQuickImage is an object representing the database table. diff --git a/db/models/publicreport.report_location.bob.go b/db/models/publicreport.report_location.bob.go index 1bbb46e9..04348b85 100644 --- a/db/models/publicreport.report_location.bob.go +++ b/db/models/publicreport.report_location.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -7,11 +7,11 @@ import ( "context" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/expr" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/expr" ) // PublicreportReportLocation is an object representing the database table. diff --git a/db/models/raster_columns.bob.go b/db/models/raster_columns.bob.go index a97fc1cb..790d214b 100644 --- a/db/models/raster_columns.bob.go +++ b/db/models/raster_columns.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -6,11 +6,11 @@ package models import ( "context" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/expr" "github.com/aarondl/opt/null" "github.com/lib/pq" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/expr" ) // RasterColumn is an object representing the database table. diff --git a/db/models/raster_overviews.bob.go b/db/models/raster_overviews.bob.go index bc5115b3..6dbf105c 100644 --- a/db/models/raster_overviews.bob.go +++ b/db/models/raster_overviews.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -6,10 +6,10 @@ package models import ( "context" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/expr" "github.com/aarondl/opt/null" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/expr" ) // RasterOverview is an object representing the database table. diff --git a/db/models/sessions.bob.go b/db/models/sessions.bob.go index 4899065f..0ebf852f 100644 --- a/db/models/sessions.bob.go +++ b/db/models/sessions.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -8,14 +8,14 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" "github.com/aarondl/opt/omit" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" ) // Session is an object representing the database table. diff --git a/db/models/spatial_ref_sys.bob.go b/db/models/spatial_ref_sys.bob.go index 651d4ad9..f4fce111 100644 --- a/db/models/spatial_ref_sys.bob.go +++ b/db/models/spatial_ref_sys.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -7,16 +7,16 @@ import ( "context" "io" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" ) // SpatialRefSy is an object representing the database table. diff --git a/db/models/user_.bob.go b/db/models/user_.bob.go index 2b8e80c2..9b74ebf1 100644 --- a/db/models/user_.bob.go +++ b/db/models/user_.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -9,20 +9,20 @@ import ( "io" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/dialect/psql/dm" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/mods" + "github.com/Gleipnir-Technology/bob/orm" + "github.com/Gleipnir-Technology/bob/types/pgtypes" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/dialect/psql/dm" - "github.com/stephenafamo/bob/dialect/psql/sm" - "github.com/stephenafamo/bob/dialect/psql/um" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/mods" - "github.com/stephenafamo/bob/orm" - "github.com/stephenafamo/bob/types/pgtypes" ) // User is an object representing the database table. diff --git a/db/prepared.go b/db/prepared.go index 29d97c1b..f66277ba 100644 --- a/db/prepared.go +++ b/db/prepared.go @@ -9,13 +9,11 @@ import ( "strings" "time" - //"github.com/stephenafamo/bob" - //"github.com/stephenafamo/bob/dialect/psql" fslayer "github.com/Gleipnir-Technology/arcgis-go/fieldseeker/layer" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" "github.com/google/uuid" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" "github.com/stephenafamo/scan" ) diff --git a/db/sql/org_by_oauth_id.bob.go b/db/sql/org_by_oauth_id.bob.go index f9b35ccd..e780eaed 100644 --- a/db/sql/org_by_oauth_id.bob.go +++ b/db/sql/org_by_oauth_id.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package sql @@ -9,11 +9,11 @@ import ( "io" "iter" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/orm" "github.com/aarondl/opt/null" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/orm" "github.com/stephenafamo/scan" ) diff --git a/db/sql/org_by_oauth_id.bob.sql b/db/sql/org_by_oauth_id.bob.sql index f0945529..800e0e6e 100644 --- a/db/sql/org_by_oauth_id.bob.sql +++ b/db/sql/org_by_oauth_id.bob.sql @@ -1,4 +1,4 @@ --- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- Code generated by BobGen psql v0.42.5. DO NOT EDIT. -- This file is meant to be re-generated in place and/or deleted at any time. -- OrgByOauthId diff --git a/db/sql/publicreport_image_with_json_by_quick_id.bob.go b/db/sql/publicreport_image_with_json_by_quick_id.bob.go index 19c257f8..747a6609 100644 --- a/db/sql/publicreport_image_with_json_by_quick_id.bob.go +++ b/db/sql/publicreport_image_with_json_by_quick_id.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package sql @@ -10,12 +10,12 @@ import ( "iter" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/orm" "github.com/aarondl/opt/null" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/orm" "github.com/stephenafamo/scan" ) diff --git a/db/sql/publicreport_image_with_json_by_quick_id.bob.sql b/db/sql/publicreport_image_with_json_by_quick_id.bob.sql index d0e37c01..21bf1e29 100644 --- a/db/sql/publicreport_image_with_json_by_quick_id.bob.sql +++ b/db/sql/publicreport_image_with_json_by_quick_id.bob.sql @@ -1,4 +1,4 @@ --- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- Code generated by BobGen psql v0.42.5. DO NOT EDIT. -- This file is meant to be re-generated in place and/or deleted at any time. -- PublicreportImageWithJSONByQuickID diff --git a/db/sql/publicreport_publicid_table.bob.go b/db/sql/publicreport_publicid_table.bob.go index f52b9b42..e8ea698b 100644 --- a/db/sql/publicreport_publicid_table.bob.go +++ b/db/sql/publicreport_publicid_table.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package sql @@ -9,11 +9,11 @@ import ( "io" "iter" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/orm" "github.com/lib/pq" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/orm" "github.com/stephenafamo/scan" ) diff --git a/db/sql/publicreport_publicid_table.bob.sql b/db/sql/publicreport_publicid_table.bob.sql index f9e1c4a6..4be6c4b0 100644 --- a/db/sql/publicreport_publicid_table.bob.sql +++ b/db/sql/publicreport_publicid_table.bob.sql @@ -1,4 +1,4 @@ --- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- Code generated by BobGen psql v0.42.5. DO NOT EDIT. -- This file is meant to be re-generated in place and/or deleted at any time. -- PublicreportIDTable diff --git a/db/sql/texts_by_senders.bob.go b/db/sql/texts_by_senders.bob.go index 3a8ce5bb..26a9cd1f 100644 --- a/db/sql/texts_by_senders.bob.go +++ b/db/sql/texts_by_senders.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package sql @@ -10,18 +10,18 @@ import ( "iter" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/orm" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/orm" "github.com/stephenafamo/scan" ) //go:embed texts_by_senders.bob.sql var formattedQueries_texts_by_senders string -var textsBySendersSQL = formattedQueries_texts_by_senders[152:393] +var textsBySendersSQL = formattedQueries_texts_by_senders[152:416] type TextsBySendersQuery = orm.ModQuery[*dialect.SelectQuery, textsBySenders, TextsBySendersRow, []TextsBySendersRow, textsBySendersTransformer] @@ -48,8 +48,9 @@ func TextsBySenders(Destination string, Source string) *TextsBySendersQuery { row.ScheduleScanByIndex(2, &t.Created) row.ScheduleScanByIndex(3, &t.Source) row.ScheduleScanByIndex(4, &t.Destination) - row.ScheduleScanByIndex(5, &t.IsWelcome) - row.ScheduleScanByIndex(6, &t.Origin) + row.ScheduleScanByIndex(5, &t.IsVisibleToLLM) + row.ScheduleScanByIndex(6, &t.IsWelcome) + row.ScheduleScanByIndex(7, &t.Origin) return &t, nil }, func(v any) (TextsBySendersRow, error) { return *(v.(*TextsBySendersRow)), nil @@ -57,22 +58,23 @@ func TextsBySenders(Destination string, Source string) *TextsBySendersQuery { }, }, Mod: bob.ModFunc[*dialect.SelectQuery](func(q *dialect.SelectQuery) { - q.AppendSelect(expressionTypArgs.subExpr(12, 97)) - q.SetTable(expressionTypArgs.subExpr(108, 122)) - q.AppendWhere(expressionTypArgs.subExpr(135, 214)) - q.CombinedOrder.AppendOrder(expressionTypArgs.subExpr(230, 241)) + q.AppendSelect(expressionTypArgs.subExpr(12, 120)) + q.SetTable(expressionTypArgs.subExpr(131, 145)) + q.AppendWhere(expressionTypArgs.subExpr(158, 237)) + q.CombinedOrder.AppendOrder(expressionTypArgs.subExpr(253, 264)) }), } } type TextsBySendersRow = struct { - ID int32 `db:"id"` - Content string `db:"content"` - Created time.Time `db:"created"` - Source string `db:"source"` - Destination string `db:"destination"` - IsWelcome bool `db:"is_welcome"` - Origin enums.CommsTextorigin `db:"origin"` + ID int32 `db:"id"` + Content string `db:"content"` + Created time.Time `db:"created"` + Source string `db:"source"` + Destination string `db:"destination"` + IsVisibleToLLM bool `db:"is_visible_to_llm"` + IsWelcome bool `db:"is_welcome"` + Origin enums.CommsTextorigin `db:"origin"` } type textsBySendersTransformer = bob.SliceTransformer[TextsBySendersRow, []TextsBySendersRow] @@ -86,8 +88,8 @@ func (o textsBySenders) args() iter.Seq[orm.ArgWithPosition] { return func(yield func(arg orm.ArgWithPosition) bool) { if !yield(orm.ArgWithPosition{ Name: "destination", - Start: 144, - Stop: 146, + Start: 167, + Stop: 169, Expression: o.Destination, }) { return @@ -95,8 +97,8 @@ func (o textsBySenders) args() iter.Seq[orm.ArgWithPosition] { if !yield(orm.ArgWithPosition{ Name: "source", - Start: 165, - Stop: 167, + Start: 188, + Stop: 190, Expression: o.Source, }) { return @@ -104,8 +106,8 @@ func (o textsBySenders) args() iter.Seq[orm.ArgWithPosition] { if !yield(orm.ArgWithPosition{ Name: "source", - Start: 191, - Stop: 193, + Start: 214, + Stop: 216, Expression: o.Source, }) { return @@ -113,8 +115,8 @@ func (o textsBySenders) args() iter.Seq[orm.ArgWithPosition] { if !yield(orm.ArgWithPosition{ Name: "destination", - Start: 212, - Stop: 214, + Start: 235, + Stop: 237, Expression: o.Destination, }) { return diff --git a/db/sql/texts_by_senders.bob.sql b/db/sql/texts_by_senders.bob.sql index fbe09f0c..ec0a7f30 100644 --- a/db/sql/texts_by_senders.bob.sql +++ b/db/sql/texts_by_senders.bob.sql @@ -1,4 +1,4 @@ --- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- Code generated by BobGen psql v0.42.5. DO NOT EDIT. -- This file is meant to be re-generated in place and/or deleted at any time. -- TextsBySenders @@ -8,6 +8,7 @@ SELECT created, source, destination, + is_visible_to_llm, is_welcome, origin FROM diff --git a/db/sql/texts_by_senders.sql b/db/sql/texts_by_senders.sql index 80fabedf..945e832e 100644 --- a/db/sql/texts_by_senders.sql +++ b/db/sql/texts_by_senders.sql @@ -5,6 +5,7 @@ SELECT created, source, destination, + is_visible_to_llm, is_welcome, origin FROM diff --git a/db/sql/trapcount_by_location_id.bob.go b/db/sql/trapcount_by_location_id.bob.go index 73475582..42afb22b 100644 --- a/db/sql/trapcount_by_location_id.bob.go +++ b/db/sql/trapcount_by_location_id.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package sql @@ -10,13 +10,13 @@ import ( "iter" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/orm" "github.com/aarondl/opt/null" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/orm" "github.com/stephenafamo/scan" ) diff --git a/db/sql/trapcount_by_location_id.bob.sql b/db/sql/trapcount_by_location_id.bob.sql index 6e76e700..cf65e9eb 100644 --- a/db/sql/trapcount_by_location_id.bob.sql +++ b/db/sql/trapcount_by_location_id.bob.sql @@ -1,4 +1,4 @@ --- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- Code generated by BobGen psql v0.42.5. DO NOT EDIT. -- This file is meant to be re-generated in place and/or deleted at any time. -- TrapCountByLocationID diff --git a/db/sql/trapdata_by_location_id_recent.bob.go b/db/sql/trapdata_by_location_id_recent.bob.go index 704f722c..b2e7c827 100644 --- a/db/sql/trapdata_by_location_id_recent.bob.go +++ b/db/sql/trapdata_by_location_id_recent.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package sql @@ -10,12 +10,12 @@ import ( "iter" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/expr" + "github.com/Gleipnir-Technology/bob/orm" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/expr" - "github.com/stephenafamo/bob/orm" "github.com/stephenafamo/scan" ) diff --git a/db/sql/trapdata_by_location_id_recent.bob.sql b/db/sql/trapdata_by_location_id_recent.bob.sql index 917e2ee6..726b461e 100644 --- a/db/sql/trapdata_by_location_id_recent.bob.sql +++ b/db/sql/trapdata_by_location_id_recent.bob.sql @@ -1,4 +1,4 @@ --- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- Code generated by BobGen psql v0.42.5. DO NOT EDIT. -- This file is meant to be re-generated in place and/or deleted at any time. -- TrapDataByLocationIDRecent diff --git a/db/sql/traplocation_by_source_id.bob.go b/db/sql/traplocation_by_source_id.bob.go index d0b437e3..668cda9c 100644 --- a/db/sql/traplocation_by_source_id.bob.go +++ b/db/sql/traplocation_by_source_id.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package sql @@ -9,11 +9,11 @@ import ( "io" "iter" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/orm" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/orm" "github.com/stephenafamo/scan" ) diff --git a/db/sql/traplocation_by_source_id.bob.sql b/db/sql/traplocation_by_source_id.bob.sql index b4610521..98ed3556 100644 --- a/db/sql/traplocation_by_source_id.bob.sql +++ b/db/sql/traplocation_by_source_id.bob.sql @@ -1,4 +1,4 @@ --- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- Code generated by BobGen psql v0.42.5. DO NOT EDIT. -- This file is meant to be re-generated in place and/or deleted at any time. -- TrapLocationBySourceID diff --git a/db/sql/update_oauth_org.bob.go b/db/sql/update_oauth_org.bob.go index 7bb34302..85591579 100644 --- a/db/sql/update_oauth_org.bob.go +++ b/db/sql/update_oauth_org.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package sql @@ -9,10 +9,10 @@ import ( "io" "iter" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/orm" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/orm" ) //go:embed update_oauth_org.bob.sql diff --git a/db/sql/update_oauth_org.bob.sql b/db/sql/update_oauth_org.bob.sql index 64bdb6cb..37337db6 100644 --- a/db/sql/update_oauth_org.bob.sql +++ b/db/sql/update_oauth_org.bob.sql @@ -1,4 +1,4 @@ --- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- Code generated by BobGen psql v0.42.5. DO NOT EDIT. -- This file is meant to be re-generated in place and/or deleted at any time. -- UpdateOauthTokenOrg diff --git a/db/sql/user_by_username.bob.go b/db/sql/user_by_username.bob.go index 4ee9ec2f..3ae2301d 100644 --- a/db/sql/user_by_username.bob.go +++ b/db/sql/user_by_username.bob.go @@ -1,4 +1,4 @@ -// Code generated by BobGen psql v0.42.1. DO NOT EDIT. +// Code generated by BobGen psql v0.42.5. DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package sql @@ -10,12 +10,12 @@ import ( "iter" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/dialect" + "github.com/Gleipnir-Technology/bob/orm" enums "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/aarondl/opt/null" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/dialect" - "github.com/stephenafamo/bob/orm" "github.com/stephenafamo/scan" ) diff --git a/db/sql/user_by_username.bob.sql b/db/sql/user_by_username.bob.sql index 9526a44e..414622c9 100644 --- a/db/sql/user_by_username.bob.sql +++ b/db/sql/user_by_username.bob.sql @@ -1,4 +1,4 @@ --- Code generated by BobGen psql v0.42.1. DO NOT EDIT. +-- Code generated by BobGen psql v0.42.5. DO NOT EDIT. -- This file is meant to be re-generated in place and/or deleted at any time. -- UserByUsername diff --git a/go.mod b/go.mod index fdc15bd7..2acd32bb 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.9 require ( github.com/Gleipnir-Technology/arcgis-go v0.0.6 + github.com/Gleipnir-Technology/bob v0.42.5 github.com/Gleipnir-Technology/go-geojson2h3/v2 v2.0.0 github.com/aarondl/opt v0.0.0-20250607033636-982744e1bd65 github.com/alexedwards/scs/pgxstore v0.0.0-20251002162104-209de6e426de @@ -24,7 +25,6 @@ require ( github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/shopspring/decimal v1.4.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/stephenafamo/bob v0.42.0 github.com/stephenafamo/scan v0.7.0 github.com/tidwall/geojson v1.4.5 github.com/twilio/twilio-go v1.29.1 @@ -81,3 +81,4 @@ require ( google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) +// replace github.com/stephenafamo/bob v0.42.0 => ../bob diff --git a/go.sum b/go.sum index f17eabfd..6a807f06 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Gleipnir-Technology/arcgis-go v0.0.6 h1:h21+ijW5NNAgRoBn2ktJwfmbgFX6f/CdlUojB4xQCWo= github.com/Gleipnir-Technology/arcgis-go v0.0.6/go.mod h1:Stx2sn5Lvuyhy4SaTQpbLNCAfenboDINi/UU5gQvz4k= +github.com/Gleipnir-Technology/bob v0.42.5 h1:fm4vH48E7scLwMFSJ4fX3+q2wSo+6Iphh+yVIrMgatE= +github.com/Gleipnir-Technology/bob v0.42.5/go.mod h1:cjUNiSRIMBsk94NQQrpYoraCe0WxIc04C8A+PcJ5z8Q= github.com/Gleipnir-Technology/go-geojson2h3/v2 v2.0.0 h1:6OMVxoiX9r7dEkIyYYKtSu7I2UDq64dww4JxJTo3p78= github.com/Gleipnir-Technology/go-geojson2h3/v2 v2.0.0/go.mod h1:W77HoRoEXUWAc24AbDHIaSH2U4vJNWgNRpEuySXzqDs= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= diff --git a/llm/client.go b/llm/client.go index af53d5f2..419b9b0c 100644 --- a/llm/client.go +++ b/llm/client.go @@ -3,6 +3,9 @@ package llm import ( "context" "fmt" + "strings" + + "github.com/maruel/genai" //"github.com/rs/zerolog/log" ) @@ -11,10 +14,59 @@ type Message struct { IsFromCustomer bool } -func GenerateNextMessage(ctx context.Context, history []Message, customer_phone string) (Message, error) { - next, err := client.continueConversation(ctx, history, customer_phone) +func GenerateNextMessage(ctx context.Context, history []Message, _handle_report_status func() (string, error), _handle_contact_district func(string), _handle_contact_supervisor func(string)) (Message, error) { + msg := convertHistory(history) + tools := genai.OptionsTools{ + Tools: []genai.ToolDef{ + { + Name: "contact_district", + Description: "Reach out to the district to get answers for a customer about their operations or schedule.", + Callback: func(ctx2 context.Context, input *ContactDistrictInput) (string, error) { + _handle_contact_district(input.Reason) + return "district has been contacted.", nil + }, + }, { + Name: "contact_supervisor", + Description: "Flag a conversation from a customer as abusive, concerning, or off-topic.", + Callback: func(ctx2 context.Context, input *ContactSupervisorInput) (string, error) { + _handle_contact_supervisor(input.Reason) + return "supervisor has been notified", nil + }, + }, { + Name: "query_report_status", + Description: "This is used to answer any questions about the current state of the mosquito nuisance report.", + Callback: func(ctx2 context.Context, input *QueryReportStatusInput) (string, error) { + return _handle_report_status() + }, + }, + }, + } + next, err := client.continueConversation(ctx, tools, msg) if err != nil { return Message{}, fmt.Errorf("Failed to generate next message: %w", err) } + return next, nil } +func convertHistory(history []Message) genai.Message { + var sb strings.Builder + sb.WriteString( + `This is a text chat conversation between a customer that's a member of the public and a mosquito abatement district. + The customer has reported a mosquito nuisance or mosquito breeding through the website report.mosquitoes.online. + Messages from the customer are prefixed with 'customer:' and reponses from the service agent servicing the request are prefixed with 'agent:'. + The agent provides clear, confident, and succint information about the state of the customer's request. + The agent answers just the questions that are asked, and prefers very short answers because the conversation is happening over SMS. + The agent rarely asks questions, preferring to just answer direct queries. + For complex or highly specific requests, the agent will need to defer to the mosquito abatement district. This will take some time because contacting the district may take a few hours to get a response. When the agent needs to contact the district, the agent should tell the customer they are reaching out to the district and to expect a delay. + When conversations start to veer away from the agent's job they should contact a supervisor. + Transcript starts:`, + ) + for _, h := range history { + if h.IsFromCustomer { + sb.WriteString(fmt.Sprintf("\n\ncustomer (%s): %s\n", h.Content)) + } else { + sb.WriteString(fmt.Sprintf("\n\nagent (%s): %s\n", h.Content)) + } + } + return genai.NewTextMessage(sb.String()) +} diff --git a/llm/log.go b/llm/log.go index 52c48989..9e305727 100644 --- a/llm/log.go +++ b/llm/log.go @@ -3,7 +3,6 @@ package llm import ( "log" "strings" - "time" "github.com/rs/zerolog" "go.mau.fi/util/exzerolog" @@ -11,14 +10,8 @@ import ( type Logger = zerolog.Logger -func createLogger() *Logger { - l := zerolog.New(zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { - //w.Out = io.Writer(buf) - w.TimeFormat = time.Stamp - })).With().Timestamp().Logger() - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix - exzerolog.SetupDefaults(&l) - return &l +func linkLogger(logger *zerolog.Logger) { + exzerolog.SetupDefaults(logger) } type ZerologWriter struct { diff --git a/llm/openai.go b/llm/openai.go index b971d1a3..1712100d 100644 --- a/llm/openai.go +++ b/llm/openai.go @@ -8,11 +8,11 @@ import ( "github.com/maruel/genai" "github.com/maruel/genai/adapters" "github.com/maruel/genai/providers/openaichat" - "github.com/rs/zerolog/log" + "github.com/rs/zerolog" ) -func CreateOpenAIClient(ctx context.Context) error { - logger := createLogger() +func CreateOpenAIClient(ctx context.Context, logger *zerolog.Logger) error { + linkLogger(logger) opts := genai.ProviderOptions{ Model: genai.ModelCheap, @@ -35,27 +35,22 @@ type openAIClient struct { log *Logger } +type ContactSupervisorInput struct { + Reason string `json:"reason"` +} + +type ContactDistrictInput struct { + Reason string `json:"reason"` +} + type QueryReportStatusInput struct { ReportID string `json:"report_id"` } var client *openAIClient -func (c *openAIClient) continueConversation(ctx context.Context, history []Message, customer_phone string) (Message, error) { - opts := genai.OptionsTools{ - Tools: []genai.ToolDef{ - { - Name: "query_report_status", - Description: "This is used to answer any questions about the current state of the mosquito nuisance report.", - Callback: func(ctx2 context.Context, input *QueryReportStatusInput) (string, error) { - return c.queryReportStatus(ctx2, customer_phone) - }, - }, - }, - } - - msg := c.convertHistory(history) - res, _, err := adapters.GenSyncWithToolCallLoop(ctx, c.client, genai.Messages{msg}, &opts) +func (c *openAIClient) continueConversation(ctx context.Context, tools genai.OptionsTools, msg genai.Message) (Message, error) { + res, _, err := adapters.GenSyncWithToolCallLoop(ctx, c.client, genai.Messages{msg}, &tools) if err != nil { return Message{}, fmt.Errorf("Failed to continue conversation: %v", err) } @@ -63,7 +58,7 @@ func (c *openAIClient) continueConversation(ctx context.Context, history []Messa for _, m := range res { // Empty responses are tool call related. if m.String() == "" { - log.Debug().Msg("Tool called") + //log.Debug().Msg("Tool called") } else { var toSay string = m.String() toSay = strings.Replace(toSay, "report-mosquitoes-online: ", "", 1) @@ -76,26 +71,3 @@ func (c *openAIClient) continueConversation(ctx context.Context, history []Messa return Message{}, nil } - -func (c *openAIClient) convertHistory(history []Message) genai.Message { - var sb strings.Builder - sb.WriteString( - `This is a text chat conversation between a customer that's a member of the public and a mosquito abatement district. - The customer has reported a mosquito nuisance or mosquito breeding through the website report.mosquitoes.online. - Messages from the customer are prefixed with 'customer:' and reponses from the service agent servicing the request are prefixed with 'agent:'. - The agent wants to provide clear, confident, and succint information about the state of the customer's request. The agent also provides general information about how members of the public can help with controlling mosquitoes. For complex or highly specific requests, the agent will need to defer to the mosquito abatement district. This will take some time because contacting the district may take a few hours to get a response. When the agent needs to contact the district, the agent should tell the customer they are reaching out to the district and to expect a delay. - Transcript starts:`, - ) - for _, h := range history { - if h.IsFromCustomer { - sb.WriteString(fmt.Sprintf("\n\ncustomer (%s): %s\n", h.Content)) - } else { - sb.WriteString(fmt.Sprintf("\n\nagent (%s): %s\n", h.Content)) - } - } - return genai.NewTextMessage(sb.String()) -} - -func (c *openAIClient) queryReportStatus(ctx context.Context, customer_phone string) (string, error) { - return "Report is scheduled for work in 3 days at 2:00pm by the district", nil -} diff --git a/main.go b/main.go index 012f9372..426ea7c3 100644 --- a/main.go +++ b/main.go @@ -77,7 +77,8 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - err = llm.CreateOpenAIClient(ctx) + openai_logger := log.With().Logger() + err = llm.CreateOpenAIClient(ctx, &openai_logger) if err != nil { log.Error().Err(err).Msg("Failed to start openAI client") os.Exit(5) diff --git a/platform/district.go b/platform/district.go index 076848d5..ec3bffab 100644 --- a/platform/district.go +++ b/platform/district.go @@ -5,12 +5,12 @@ import ( "errors" "fmt" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/sm" ) func DistrictForLocation(ctx context.Context, lng float64, lat float64) (*models.ImportDistrict, *models.Organization, error) { diff --git a/platform/text.go b/platform/text.go index cebc16e4..8d76da0f 100644 --- a/platform/text.go +++ b/platform/text.go @@ -6,6 +6,8 @@ import ( "strings" "time" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" @@ -38,9 +40,9 @@ func HandleTextMessage(from string, to string, body string) { log.Error().Err(err).Msg("Failed to handle message") return } + body_l := strings.TrimSpace(strings.ToLower(body)) // We don't know if they're subscribed or not. if subscribed == nil { - body_l := strings.TrimSpace(strings.ToLower(body)) switch body_l { case "stop": setSubscribed(ctx, src, false) @@ -54,20 +56,25 @@ func HandleTextMessage(from string, to string, body string) { log.Error().Err(err).Msg("Failed to add reiteration to the text log") return }*/ - err = sendText(ctx, src, dst, content, enums.CommsTextoriginReiteration, false) + err = sendText(ctx, dst, src, content, enums.CommsTextoriginReiteration, false) if err != nil { log.Error().Err(err).Msg("Failed to resend initial prompt.") } } return } - previous_messages, err := loadPreviousMessages(ctx, dst, src) + // If we get the super-special "reset conversation" then wipe the LLM's memory + if body_l == "reset conversation" { + handleResetConversation(ctx, src, dst) + return + } + previous_messages, err := loadPreviousMessagesForLLM(ctx, dst, src) if err != nil { log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to get previous messages") return } log.Info().Int("len", len(previous_messages)).Msg("passing") - next_message, err := llm.GenerateNextMessage(ctx, previous_messages, src) + next_message, err := generateNextMessage(ctx, previous_messages, src) if err != nil { log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to generate next message") return @@ -79,7 +86,7 @@ func HandleTextMessage(from string, to string, body string) { return } */ - err = sendText(ctx, dst, src, next_message.Content, enums.CommsTextoriginLLM, false) + err = sendText(ctx, src, dst, next_message.Content, enums.CommsTextoriginLLM, false) if err != nil { log.Error().Err(err).Str("src", src).Str("dst", dst).Str("content", next_message.Content).Msg("Failed to send response text") return @@ -166,6 +173,19 @@ func ensureInDB(ctx context.Context, destination string) (err error) { return nil } +func generateNextMessage(ctx context.Context, history []llm.Message, customer_phone string) (llm.Message, error) { + _handle_report_status := func() (string, error) { + return "Report: ABCD-1234-5678, Status: scheduled, Appointment: Wednesday 3:30pm", nil + } + _handle_contact_district := func(reason string) { + log.Warn().Str("reason", reason).Msg("Contacting district") + } + _handle_contact_supervisor := func(reason string) { + log.Warn().Str("reason", reason).Msg("Contacting supervisor") + } + return llm.GenerateNextMessage(ctx, history, _handle_report_status, _handle_contact_district, _handle_contact_supervisor) +} + // Translate from Twilio's representation of a RCS message sender to our concept of a phone number // From: rcs:dev_report_mosquitoes_online_dosrvwxm_agent // To: +16235525879 @@ -188,18 +208,38 @@ func handleWaitingTextJobs(ctx context.Context, src string) { log.Info().Str("src", src).Msg("Pretend handle waiting jobs") } +func handleResetConversation(ctx context.Context, src string, dst string) { + err := wipeLLMMemory(ctx, src, dst) + if err != nil { + log.Error().Err(err).Str("src", src).Str("dst", dst).Msg("Failed to wipe memory") + content := "Failed to wip memory" + err = sendText(ctx, dst, src, content, enums.CommsTextoriginCommandResponse, false) + if err != nil { + log.Error().Err(err).Str("src", src).Str("dst", dst).Msg("Failed to indicated memory wipe failure.") + } + return + } + content := "LLM memory wiped" + err = sendText(ctx, dst, src, content, enums.CommsTextoriginCommandResponse, false) + if err != nil { + log.Error().Err(err).Str("src", src).Str("dst", dst).Msg("Failed to indicated memory wiped.") + return + } + log.Info().Err(err).Str("src", src).Str("dst", dst).Msg("Wiped LLM memory") +} func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool) (log *models.CommsTextLog, err error) { log, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ //ID: - Content: omit.From(content), - Created: omit.From(time.Now()), - Destination: omit.From(destination), - IsWelcome: omit.From(is_welcome), - Origin: omit.From(origin), - Source: omit.From(source), - TwilioSid: omitnull.FromPtr[string](nil), - TwilioStatus: omit.From(""), + Content: omit.From(content), + Created: omit.From(time.Now()), + Destination: omit.From(destination), + IsVisibleToLLM: omit.From(true), + IsWelcome: omit.From(is_welcome), + Origin: omit.From(origin), + Source: omit.From(source), + TwilioSid: omitnull.FromPtr[string](nil), + TwilioStatus: omit.From(""), }).One(ctx, db.PGInstance.BobDB) return log, err @@ -217,7 +257,7 @@ func isSubscribed(ctx context.Context, src string) (*bool, error) { return &result, nil } -func loadPreviousMessages(ctx context.Context, dst, src string) ([]llm.Message, error) { +func loadPreviousMessagesForLLM(ctx context.Context, dst, src string) ([]llm.Message, error) { messages, err := sql.TextsBySenders(dst, src).All(ctx, db.PGInstance.BobDB) results := make([]llm.Message, 0) if err != nil { @@ -225,11 +265,13 @@ func loadPreviousMessages(ctx context.Context, dst, src string) ([]llm.Message, } log.Info().Int("count", len(messages)).Str("src", src).Str("dst", dst).Msg("Found previous messages") for _, m := range messages { - is_from_customer := (m.Source == src) - results = append(results, llm.Message{ - IsFromCustomer: is_from_customer, - Content: m.Content, - }) + if m.IsVisibleToLLM { + is_from_customer := (m.Source == src) + results = append(results, llm.Message{ + IsFromCustomer: is_from_customer, + Content: m.Content, + }) + } } return results, nil } @@ -282,3 +324,25 @@ func setSubscribed(ctx context.Context, src string, is_subscribed bool) error { log.Info().Str("src", src).Bool("is_subscribed", is_subscribed).Msg("Set number subscribed") return nil } + +func wipeLLMMemory(ctx context.Context, src string, dst string) error { + rows, err := sql.TextsBySenders(dst, src).All(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to query for texts: %w", err) + } + ids := make([]int32, 0) + for _, r := range rows { + ids = append(ids, r.ID) + } + _, err = models.CommsTextLogs.Update( + um.Where( + models.CommsTextLogs.Columns.ID.EQ(psql.Any(ids)), + ), + um.SetCol("is_visible_to_llm").ToArg(false), + ).Exec(ctx, db.PGInstance.BobDB) + if err != nil { + return fmt.Errorf("Failed to update texts: %w", err) + } + + return nil +} diff --git a/public-report/image-upload.go b/public-report/image-upload.go index b566f06e..a6ccb71d 100644 --- a/public-report/image-upload.go +++ b/public-report/image-upload.go @@ -13,6 +13,9 @@ import ( "net/http" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/userfile" "github.com/aarondl/opt/omit" @@ -20,9 +23,6 @@ import ( "github.com/google/uuid" "github.com/rs/zerolog/log" "github.com/rwcarlsen/goexif/exif" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/um" //exif "github.com/rwcarlsen/goexif/exif" //"github.com/dsoprea/go-exif-extra/format" ) diff --git a/public-report/pool.go b/public-report/pool.go index 6872c8a7..087f5301 100644 --- a/public-report/pool.go +++ b/public-report/pool.go @@ -6,6 +6,9 @@ import ( "strconv" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" @@ -13,9 +16,6 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/htmlpage" "github.com/aarondl/opt/omit" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/um" ) type ContextPool struct { diff --git a/public-report/quick.go b/public-report/quick.go index beb95308..4feea1a5 100644 --- a/public-report/quick.go +++ b/public-report/quick.go @@ -7,6 +7,9 @@ import ( "strconv" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/um" "github.com/Gleipnir-Technology/nidus-sync/background" "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" @@ -19,9 +22,6 @@ import ( "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/um" ) type ContentQuick struct{} diff --git a/public-report/status.go b/public-report/status.go index 02b908be..7b0f39ae 100644 --- a/public-report/status.go +++ b/public-report/status.go @@ -7,6 +7,9 @@ import ( "strings" "time" + "github.com/Gleipnir-Technology/bob" + "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" @@ -14,9 +17,6 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/htmlpage" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/sm" "github.com/stephenafamo/scan" /* "github.com/Gleipnir-Technology/nidus-sync/db" diff --git a/query.go b/query.go index a13d70d8..0b50022e 100644 --- a/query.go +++ b/query.go @@ -5,8 +5,8 @@ import ( "context" "fmt" "io" - //"github.com/stephenafamo/bob" - //"github.com/stephenafamo/bob/dialect/psql" + //"github.com/Gleipnir-Technology/bob" + //"github.com/Gleipnir-Technology/bob/dialect/psql" ) type QueryWriter interface { diff --git a/sync/dash.go b/sync/dash.go index c0233739..54ab9abd 100644 --- a/sync/dash.go +++ b/sync/dash.go @@ -7,6 +7,7 @@ import ( "net/http" "time" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/background" "github.com/Gleipnir-Technology/nidus-sync/config" @@ -16,7 +17,6 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/htmlpage" "github.com/go-chi/chi/v5" "github.com/google/uuid" - "github.com/stephenafamo/bob/dialect/psql/sm" "github.com/uber/h3-go/v4" ) diff --git a/sync/utils.go b/sync/utils.go index 94f2dcd7..4af735d6 100644 --- a/sync/utils.go +++ b/sync/utils.go @@ -6,14 +6,14 @@ import ( "strings" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" + "github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/sql" "github.com/Gleipnir-Technology/nidus-sync/notification" "github.com/google/uuid" - "github.com/stephenafamo/bob" - "github.com/stephenafamo/bob/dialect/psql" - "github.com/stephenafamo/bob/dialect/psql/sm" "github.com/uber/h3-go/v4" ) From 9914274d42f00b6144807f0cb5e08c3b907c077a Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 27 Jan 2026 19:56:26 +0000 Subject: [PATCH 0147/1513] Wire in agent to the reporter texting system Also rework the so the platform absorbs all the business logic that was going in the wrong place. --- api/twilio.go | 6 +- auth/auth.go | 2 +- background/background.go | 2 +- background/email.go | 1 + background/text.go | 5 +- comms/text/text.go | 9 +-- llm/log.go | 4 +- main.go | 4 +- {comms => platform}/text/db.go | 0 {comms => platform}/text/job.go | 0 {comms => platform}/text/llm.go | 0 .../text/report-subscription.go | 48 +++++------ platform/{ => text}/text.go | 79 +++++++++++-------- public-report/quick.go | 2 +- 14 files changed, 86 insertions(+), 76 deletions(-) rename {comms => platform}/text/db.go (100%) rename {comms => platform}/text/job.go (100%) rename {comms => platform}/text/llm.go (100%) rename {comms => platform}/text/report-subscription.go (53%) rename platform/{ => text}/text.go (87%) diff --git a/api/twilio.go b/api/twilio.go index 91ee54ba..ef1d205c 100644 --- a/api/twilio.go +++ b/api/twilio.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" - "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/text" "github.com/rs/zerolog/log" "github.com/twilio/twilio-go/twiml" ) @@ -18,7 +18,7 @@ func twilioStatusPost(w http.ResponseWriter, r *http.Request) { message_sid := r.PostFormValue("MessageSid") message_status := r.PostFormValue("MessageStatus") log.Info().Str("sid", message_sid).Str("status", message_status).Msg("Updated message status") - platform.UpdateMessageStatus(message_sid, message_status) + text.UpdateMessageStatus(message_sid, message_status) fmt.Fprintf(w, "") } func twilioTextPost(w http.ResponseWriter, r *http.Request) { @@ -43,7 +43,7 @@ func twilioTextPost(w http.ResponseWriter, r *http.Request) { log.Info().Str("message_sid", message_sid).Str("account_sid", account_sid).Str("messaging_service_sid", messaging_service_sid).Str("from", from).Str("to_", to_).Str("body", body).Str("num_media", num_media).Str("num_segments", num_segments).Str("media_content_type0", media_content_type0).Str("media_url0", media_url0).Str("from_city", from_city).Str("from_state", from_state).Str("from_zip", from_zip).Str("from_country", from_country).Str("to_city", to_city).Str("to_state", to_state).Str("to_zip", to_zip).Str("to_country", to_country).Msg("got text") twiml, _ := twiml.Messages([]twiml.Element{}) - go platform.HandleTextMessage(from, to_, body) + go text.HandleTextMessage(from, to_, body) w.Header().Set("Content-Type", "text/xml") fmt.Fprintf(w, "%s", twiml) } diff --git a/auth/auth.go b/auth/auth.go index df88736b..6599d328 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -178,7 +178,7 @@ func findUser(ctx context.Context, user_id int) (*models.User, error) { return nil, err } } - log.Info().Int32("user_id", user.ID).Int32("org_id", user.OrganizationID).Msg("Found user") + //log.Info().Int32("user_id", user.ID).Int32("org_id", user.OrganizationID).Msg("Found user") return user, err } diff --git a/background/background.go b/background/background.go index a5c44584..92705b81 100644 --- a/background/background.go +++ b/background/background.go @@ -5,7 +5,7 @@ import ( "sync" "github.com/Gleipnir-Technology/nidus-sync/comms/email" - "github.com/Gleipnir-Technology/nidus-sync/comms/text" + "github.com/Gleipnir-Technology/nidus-sync/platform/text" ) var waitGroup sync.WaitGroup diff --git a/background/email.go b/background/email.go index ea05ac9f..ccdd464c 100644 --- a/background/email.go +++ b/background/email.go @@ -27,6 +27,7 @@ func enqueueJobEmail(job email.Job) { func startWorkerEmail(ctx context.Context, channel chan email.Job) { go func() { + log.Info().Msg("Email worker started") for { select { case <-ctx.Done(): diff --git a/background/text.go b/background/text.go index 91308769..8e9d920a 100644 --- a/background/text.go +++ b/background/text.go @@ -3,8 +3,8 @@ package background import ( "context" - "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/platform/text" "github.com/rs/zerolog/log" ) @@ -29,10 +29,11 @@ func enqueueJobText(job text.Job) { func startWorkerText(ctx context.Context, channel chan text.Job) { go func() { + log.Info().Msg("Text worker started") for { select { case <-ctx.Done(): - log.Info().Msg("Email worker shutting down.") + log.Info().Msg("Text worker shutting down.") return case job := <-channel: text.Handle(ctx, job) diff --git a/comms/text/text.go b/comms/text/text.go index c98e33fc..018cfdd8 100644 --- a/comms/text/text.go +++ b/comms/text/text.go @@ -6,18 +6,11 @@ import ( "fmt" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/nyaruka/phonenumbers" "github.com/rs/zerolog/log" "github.com/twilio/twilio-go" twilioApi "github.com/twilio/twilio-go/rest/api/v2010" ) -type E164 = phonenumbers.PhoneNumber - -func ParsePhoneNumber(input string) (*E164, error) { - return phonenumbers.Parse(input, "US") -} - func SendText(ctx context.Context, source string, destination string, message string) (string, error) { client := twilio.NewRestClient() @@ -31,11 +24,11 @@ func SendText(ctx context.Context, source string, destination string, message st if err != nil { return "", fmt.Errorf("Failed to create message to %s: %w", destination, err) } - //log.Info().Str("dest", destination).Str("sid", *resp.Body).Msg("Text message response") if resp.Sid == nil { log.Warn().Str("src", source).Str("dst", destination).Msg("Text message sid is nil") return "", nil } + log.Info().Str("src", source).Str("dst", destination).Str("message", message).Str("sid", *resp.Sid).Msg("Created text message") return *resp.Sid, nil } diff --git a/llm/log.go b/llm/log.go index 9e305727..984bd88d 100644 --- a/llm/log.go +++ b/llm/log.go @@ -5,13 +5,13 @@ import ( "strings" "github.com/rs/zerolog" - "go.mau.fi/util/exzerolog" + //"go.mau.fi/util/exzerolog" ) type Logger = zerolog.Logger func linkLogger(logger *zerolog.Logger) { - exzerolog.SetupDefaults(logger) + //exzerolog.SetupDefaults(logger) } type ZerologWriter struct { diff --git a/main.go b/main.go index 426ea7c3..3c695f90 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/llm" - "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/text" "github.com/Gleipnir-Technology/nidus-sync/public-report" nidussync "github.com/Gleipnir-Technology/nidus-sync/sync" "github.com/go-chi/chi/v5" @@ -48,7 +48,7 @@ func main() { os.Exit(3) } - err = platform.TextStoreSources() + err = text.StoreSources() if err != nil { log.Error().Err(err).Msg("Failed to store text source phone numbers") os.Exit(4) diff --git a/comms/text/db.go b/platform/text/db.go similarity index 100% rename from comms/text/db.go rename to platform/text/db.go diff --git a/comms/text/job.go b/platform/text/job.go similarity index 100% rename from comms/text/job.go rename to platform/text/job.go diff --git a/comms/text/llm.go b/platform/text/llm.go similarity index 100% rename from comms/text/llm.go rename to platform/text/llm.go diff --git a/comms/text/report-subscription.go b/platform/text/report-subscription.go similarity index 53% rename from comms/text/report-subscription.go rename to platform/text/report-subscription.go index 12328338..3fe39e84 100644 --- a/comms/text/report-subscription.go +++ b/platform/text/report-subscription.go @@ -4,8 +4,7 @@ import ( "context" "fmt" - //"github.com/Gleipnir-Technology/nidus-sync/db/enums" - //"github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/nyaruka/phonenumbers" //"github.com/rs/zerolog/log" ) @@ -44,32 +43,33 @@ func (j jobReportSubscription) source() string { } func sendReportSubscription(ctx context.Context, job Job) error { - /* - j, ok := job.(jobReportSubscription) - if !ok { - return fmt.Errorf("job is not for report subscription confirmation") - } + j, ok := job.(jobReportSubscription) + if !ok { + return fmt.Errorf("job is not for report subscription confirmation") + } - sub, err := isSubscribed(ctx, job.destination()) + sub, err := isSubscribed(ctx, job.destination()) + if err != nil { + return fmt.Errorf("Failed to check if subscribed: %w", err) + } + if sub == nil { + err = delayMessage(ctx, j.source(), j.destination(), j.content(), enums.CommsTextjobtypeReportConfirmation) if err != nil { - return fmt.Errorf("Failed to check if subscribed: %w", err) + return fmt.Errorf("Failed to delay report subscription message: %w", err) } - if !sub { - err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction, false) - if err != nil { - return fmt.Errorf("Failed to send report subscription confirmation: %w", err) - } - } else { - err = delayMessage(ctx, j.source(), j.destination(), j.content(), enums.CommsTextjobtypeReportConfirmation) - if err != nil { - return fmt.Errorf("Failed to delay report subscription message: %w", err) - } - err := ensureInitialText(ctx, j.source(), j.destination()) - if err != nil { - return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) - } + err := ensureInitialText(ctx, j.source(), j.destination()) + if err != nil { + return fmt.Errorf("Failed to ensure initial text has been sent: %w", err) } return nil - */ + } + if *sub { + err = sendText(ctx, j.source(), j.destination(), j.content(), enums.CommsTextoriginWebsiteAction, false, true) + if err != nil { + return fmt.Errorf("Failed to send report subscription confirmation: %w", err) + } + } else { + resendInitialText(ctx, j.source(), j.destination()) + } return nil } diff --git a/platform/text.go b/platform/text/text.go similarity index 87% rename from platform/text.go rename to platform/text/text.go index 8d76da0f..84536653 100644 --- a/platform/text.go +++ b/platform/text/text.go @@ -1,4 +1,4 @@ -package platform +package text import ( "context" @@ -21,6 +21,8 @@ import ( "github.com/rs/zerolog/log" ) +type E164 = phonenumbers.PhoneNumber + func HandleTextMessage(from string, to string, body string) { ctx := context.Background() type_, src := splitPhoneSource(from) @@ -30,7 +32,7 @@ func HandleTextMessage(from string, to string, body string) { return } - _, err = insertTextLog(ctx, body, dst, src, enums.CommsTextoriginCustomer, false) + _, err = insertTextLog(ctx, body, dst, src, enums.CommsTextoriginCustomer, false, true) if err != nil { log.Error().Err(err).Str("dst", dst).Msg("Failed to add text message log") return @@ -51,12 +53,7 @@ func HandleTextMessage(from string, to string, body string) { handleWaitingTextJobs(ctx, src) default: content := "I have to start with either 'YES' or 'STOP' first, Which do you want?" - /*err := insertTextLog(ctx, body, src, dst, enums.CommsTextoriginReiteration, false) - if err != nil { - log.Error().Err(err).Msg("Failed to add reiteration to the text log") - return - }*/ - err = sendText(ctx, dst, src, content, enums.CommsTextoriginReiteration, false) + err = sendText(ctx, dst, src, content, enums.CommsTextoriginReiteration, false, false) if err != nil { log.Error().Err(err).Msg("Failed to resend initial prompt.") } @@ -79,14 +76,7 @@ func HandleTextMessage(from string, to string, body string) { log.Error().Err(err).Str("dst", dst).Str("src", from).Msg("Failed to generate next message") return } - /* - err = insertTextLog(ctx, next_message.Content, src, dst, enums.CommsTextoriginLLM, false) - if err != nil { - log.Error().Err(err).Str("dst", dst).Msg("Failed to insert new text message to the text log") - return - } - */ - err = sendText(ctx, src, dst, next_message.Content, enums.CommsTextoriginLLM, false) + err = sendText(ctx, dst, src, next_message.Content, enums.CommsTextoriginLLM, false, true) if err != nil { log.Error().Err(err).Str("src", src).Str("dst", dst).Str("content", next_message.Content).Msg("Failed to send response text") return @@ -94,7 +84,11 @@ func HandleTextMessage(from string, to string, body string) { log.Info().Str("from", from).Str("from-type", type_).Str("to", to).Str("src", src).Str("dst", dst).Str("body", body).Str("reply", next_message.Content).Msg("Handled text message") } -func TextStoreSources() error { +func ParsePhoneNumber(input string) (*E164, error) { + return phonenumbers.Parse(input, "US") +} + +func StoreSources() error { ctx := context.TODO() src := phonenumbers.Format(&config.PhoneNumberReport, phonenumbers.E164) return ensureInDB(ctx, src) @@ -132,9 +126,32 @@ func delayMessage(ctx context.Context, source string, destination string, conten return nil } +func resendInitialText(ctx context.Context, src string, dst string) error { + phone, err := models.FindCommsPhone(ctx, db.PGInstance.BobDB, dst) + if err != nil { + return fmt.Errorf("Failed to find phone %s: %w", dst, err) + } + err = phone.Update(ctx, db.PGInstance.BobDB, &models.CommsPhoneSetter{ + IsSubscribed: omitnull.FromPtr[bool](nil), + }) + if err != nil { + return fmt.Errorf("Failed to clear subscription on phone %s: %w", dst, err) + } + return nil +} + +func sendInitialText(ctx context.Context, src string, dst string) error { + content := "Welcome to Report Mosquitoes Online. We received your request and want to confirm text updates. Reply YES to continue. Reply STOP at any time to unsubscribe" + origin := enums.CommsTextoriginWebsiteAction + err := sendText(ctx, src, dst, content, origin, true, true) + if err != nil { + return fmt.Errorf("Failed to send initial confirmation: %w", err) + } + return nil +} + func ensureInitialText(ctx context.Context, src string, dst string) error { // - origin := enums.CommsTextoriginWebsiteAction rows, err := models.CommsTextLogs.Query( models.SelectWhere.CommsTextLogs.Destination.EQ(dst), models.SelectWhere.CommsTextLogs.IsWelcome.EQ(true), @@ -145,12 +162,7 @@ func ensureInitialText(ctx context.Context, src string, dst string) error { if len(rows) > 0 { return nil } - content := "Welcome to Report Mosquitoes Online. We received your request and want to confirm text updates. Reply YES to continue. Reply STOP at any time to unsubscribe" - err = sendText(ctx, src, dst, content, origin, true) - if err != nil { - return fmt.Errorf("Failed to send initial confirmation: %w", err) - } - return nil + return sendInitialText(ctx, src, dst) } func ensureInDB(ctx context.Context, destination string) (err error) { @@ -213,14 +225,14 @@ func handleResetConversation(ctx context.Context, src string, dst string) { if err != nil { log.Error().Err(err).Str("src", src).Str("dst", dst).Msg("Failed to wipe memory") content := "Failed to wip memory" - err = sendText(ctx, dst, src, content, enums.CommsTextoriginCommandResponse, false) + err = sendText(ctx, dst, src, content, enums.CommsTextoriginCommandResponse, false, false) if err != nil { log.Error().Err(err).Str("src", src).Str("dst", dst).Msg("Failed to indicated memory wipe failure.") } return } content := "LLM memory wiped" - err = sendText(ctx, dst, src, content, enums.CommsTextoriginCommandResponse, false) + err = sendText(ctx, dst, src, content, enums.CommsTextoriginCommandResponse, false, false) if err != nil { log.Error().Err(err).Str("src", src).Str("dst", dst).Msg("Failed to indicated memory wiped.") return @@ -228,13 +240,13 @@ func handleResetConversation(ctx context.Context, src string, dst string) { log.Info().Err(err).Str("src", src).Str("dst", dst).Msg("Wiped LLM memory") } -func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool) (log *models.CommsTextLog, err error) { +func insertTextLog(ctx context.Context, content string, destination string, source string, origin enums.CommsTextorigin, is_welcome bool, is_visible_to_llm bool) (log *models.CommsTextLog, err error) { log, err = models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ //ID: Content: omit.From(content), Created: omit.From(time.Now()), Destination: omit.From(destination), - IsVisibleToLLM: omit.From(true), + IsVisibleToLLM: omit.From(is_visible_to_llm), IsWelcome: omit.From(is_welcome), Origin: omit.From(origin), Source: omit.From(source), @@ -263,7 +275,6 @@ func loadPreviousMessagesForLLM(ctx context.Context, dst, src string) ([]llm.Mes if err != nil { return results, fmt.Errorf("Failed to get message history for %s and %s: %w", dst, src, err) } - log.Info().Int("count", len(messages)).Str("src", src).Str("dst", dst).Msg("Found previous messages") for _, m := range messages { if m.IsVisibleToLLM { is_from_customer := (m.Source == src) @@ -276,12 +287,12 @@ func loadPreviousMessagesForLLM(ctx context.Context, dst, src string) ([]llm.Mes return results, nil } -func sendText(ctx context.Context, source string, destination string, message string, origin enums.CommsTextorigin, is_welcome bool) error { +func sendText(ctx context.Context, source string, destination string, message string, origin enums.CommsTextorigin, is_welcome bool, is_visible_to_llm bool) error { err := ensureInDB(ctx, destination) if err != nil { return fmt.Errorf("Failed to ensure text message destination is in the DB: %w", err) } - log, err := insertTextLog(ctx, message, destination, source, origin, is_welcome) + l, err := insertTextLog(ctx, message, destination, source, origin, is_welcome, is_visible_to_llm) if err != nil { return fmt.Errorf("Failed to insert text message in the DB: %w", err) } @@ -289,10 +300,14 @@ func sendText(ctx context.Context, source string, destination string, message st if err != nil { return fmt.Errorf("Failed to send text message: %w", err) } - err = log.Update(ctx, db.PGInstance.BobDB, &models.CommsTextLogSetter{ + err = l.Update(ctx, db.PGInstance.BobDB, &models.CommsTextLogSetter{ TwilioSid: omitnull.From(sid), TwilioStatus: omit.From("created"), }) + if err != nil { + return fmt.Errorf("Failed to update text Twilio status: %w", err) + } + log.Info().Int32("id", l.ID).Bool("is_visible_to_llm", is_visible_to_llm).Str("message", message).Msg("inserted text log") return nil } diff --git a/public-report/quick.go b/public-report/quick.go index 4feea1a5..708b5953 100644 --- a/public-report/quick.go +++ b/public-report/quick.go @@ -11,7 +11,6 @@ import ( "github.com/Gleipnir-Technology/bob/dialect/psql" "github.com/Gleipnir-Technology/bob/dialect/psql/um" "github.com/Gleipnir-Technology/nidus-sync/background" - "github.com/Gleipnir-Technology/nidus-sync/comms/text" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" @@ -19,6 +18,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/h3utils" "github.com/Gleipnir-Technology/nidus-sync/htmlpage" "github.com/Gleipnir-Technology/nidus-sync/platform" + "github.com/Gleipnir-Technology/nidus-sync/platform/text" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" From a42c5824af5ec0ab7a3b31b4488ac4aabead530d Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 27 Jan 2026 23:25:51 +0000 Subject: [PATCH 0148/1513] Add district to LLM context, be more aggressive about trimming agent: --- llm/client.go | 14 +++++++++++--- platform/text/text.go | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/llm/client.go b/llm/client.go index 419b9b0c..832b5be3 100644 --- a/llm/client.go +++ b/llm/client.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/maruel/genai" - //"github.com/rs/zerolog/log" + "github.com/rs/zerolog/log" ) type Message struct { @@ -45,6 +45,14 @@ func GenerateNextMessage(ctx context.Context, history []Message, _handle_report_ if err != nil { return Message{}, fmt.Errorf("Failed to generate next message: %w", err) } + trimmed, found := strings.CutPrefix(next.Content, "agent:") + if !found { + trimmed, found = strings.CutPrefix(next.Content, "Agent:") + if !found { + log.Warn().Str("content", next.Content).Msg("No 'agent:' prefix on next message") + } + } + next.Content = trimmed return next, nil } @@ -63,9 +71,9 @@ func convertHistory(history []Message) genai.Message { ) for _, h := range history { if h.IsFromCustomer { - sb.WriteString(fmt.Sprintf("\n\ncustomer (%s): %s\n", h.Content)) + sb.WriteString(fmt.Sprintf("\n\ncustomer: %s\n", h.Content)) } else { - sb.WriteString(fmt.Sprintf("\n\nagent (%s): %s\n", h.Content)) + sb.WriteString(fmt.Sprintf("\n\nagent: %s\n", h.Content)) } } return genai.NewTextMessage(sb.String()) diff --git a/platform/text/text.go b/platform/text/text.go index 84536653..bcca0bc1 100644 --- a/platform/text/text.go +++ b/platform/text/text.go @@ -187,7 +187,7 @@ func ensureInDB(ctx context.Context, destination string) (err error) { func generateNextMessage(ctx context.Context, history []llm.Message, customer_phone string) (llm.Message, error) { _handle_report_status := func() (string, error) { - return "Report: ABCD-1234-5678, Status: scheduled, Appointment: Wednesday 3:30pm", nil + return "Report: ABCD-1234-5678, District: Delta MVCD, Status: scheduled, Appointment: Wednesday 3:30pm", nil } _handle_contact_district := func(reason string) { log.Warn().Str("reason", reason).Msg("Contacting district") From 182175254ed9b6c96202193713956c6f732393ea Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 28 Jan 2026 14:57:50 +0000 Subject: [PATCH 0149/1513] Clean up old SMS callback endpoints --- llm/client.go | 2 +- sync/routes.go | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/llm/client.go b/llm/client.go index 832b5be3..1bad4e63 100644 --- a/llm/client.go +++ b/llm/client.go @@ -67,7 +67,7 @@ func convertHistory(history []Message) genai.Message { The agent rarely asks questions, preferring to just answer direct queries. For complex or highly specific requests, the agent will need to defer to the mosquito abatement district. This will take some time because contacting the district may take a few hours to get a response. When the agent needs to contact the district, the agent should tell the customer they are reaching out to the district and to expect a delay. When conversations start to veer away from the agent's job they should contact a supervisor. - Transcript starts:`, + Transcript:\n`, ) for _, h := range history { if h.IsFromCustomer { diff --git a/sync/routes.go b/sync/routes.go index f980dcdb..645ecf70 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -54,19 +54,14 @@ func Router() chi.Router { r.Get("/qr-code/report/{code}", getQRCodeReport) r.Get("/signin", getSignin) r.Post("/signin", postSignin) - r.Method("GET", "/signout", auth.NewEnsureAuth(getSignout)) r.Get("/signup", getSignup) r.Post("/signup", postSignup) - r.Get("/sms", getSMS) - r.Post("/sms", postSMS) - r.Get("/sms.php", getSMS) - r.Get("/sms/{org}", getSMS) - r.Post("/sms/{org}", postSMS) // Authenticated endpoints r.Route("/api", api.AddRoutes) r.Method("GET", "/cell/{cell}", auth.NewEnsureAuth(getCellDetails)) r.Method("GET", "/settings", auth.NewEnsureAuth(getSettings)) + r.Method("GET", "/signout", auth.NewEnsureAuth(getSignout)) r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) r.Method("GET", "/trap/{globalid}", auth.NewEnsureAuth(getTrap)) From bdb3c80ad7639b2e7c357cbcda572599ec0a1085 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 28 Jan 2026 14:58:13 +0000 Subject: [PATCH 0150/1513] Add Basic text message review mock --- sync/routes.go | 1 + sync/template/text-messages.html | 143 +++++++++++++++++++++++++++++++ sync/text.go | 28 ++++++ 3 files changed, 172 insertions(+) create mode 100644 sync/template/text-messages.html create mode 100644 sync/text.go diff --git a/sync/routes.go b/sync/routes.go index 645ecf70..feba55ce 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -64,6 +64,7 @@ func Router() chi.Router { r.Method("GET", "/signout", auth.NewEnsureAuth(getSignout)) r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) r.Method("GET", "/trap/{globalid}", auth.NewEnsureAuth(getTrap)) + r.Method("GET", "/text/{destination}", auth.NewEnsureAuth(getTextMessages)) htmlpage.AddStaticRoute(r, "/static") return r diff --git a/sync/template/text-messages.html b/sync/template/text-messages.html new file mode 100644 index 00000000..c38c243e --- /dev/null +++ b/sync/template/text-messages.html @@ -0,0 +1,143 @@ +{{template "authenticated.html" .}} + +{{define "title"}}Dash{{end}} +{{define "extraheader"}} + +{{end}} +{{define "content"}} +
+ +
+
+
+
+
+ User avatar +
+
+
Chat with Sarah Johnson
+ Last active 5 minutes ago +
+
+
+
+
+ + +
+
+
+
+ +
+ Today, 2:30 PM +
+ + +
+ Receiver avatar +
+
+ Hi there! How's the project coming along? +
+
2:31 PM
+
+
+ + +
+ Sender avatar +
+
+ Hey! It's going pretty well. I'm working on the UI mockups right now. +
+
2:33 PM
+
+
+ + +
+ Receiver avatar +
+
+ That's great to hear! When do you think you'll be able to share them with the team? +
+
2:35 PM
+
+
+ + +
+ Sender avatar +
+
+ I'm hoping to have something ready by tomorrow afternoon. I'm just working out some details with the responsive design. +
+
2:36 PM
+
+
+ + +
+ Sender avatar +
+
+ Do you have any specific feedback on the initial concept I shared last week? +
+
2:37 PM
+
+
+ + +
+ Receiver avatar +
+
+ Yes! The team loved it. The color scheme was particularly well received. We just had some minor suggestions about the navigation that I can share during our next call. +
+
2:40 PM
+
+
+ + +
+ Sender avatar +
+
+ That sounds great! Looking forward to the feedback. +
+
2:41 PM
+
+
+
+
+
+
+
+{{end}} diff --git a/sync/text.go b/sync/text.go new file mode 100644 index 00000000..2d91cbea --- /dev/null +++ b/sync/text.go @@ -0,0 +1,28 @@ +package sync + +import ( + "net/http" + + "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/Gleipnir-Technology/nidus-sync/htmlpage" +) + +type ContentTextMessages struct { + User User +} + +var ( + textMessagesT = buildTemplate("text-messages", "authenticated") +) + +func getTextMessages(w http.ResponseWriter, r *http.Request, u *models.User) { + userContent, err := contentForUser(r.Context(), u) + if err != nil { + respondError(w, "Failed to get user", err, http.StatusInternalServerError) + return + } + content := ContentTextMessages{ + User: userContent, + } + htmlpage.RenderOrError(w, textMessagesT, content) +} From 082fdeebdd35d969ca7cc73a68c3a4dfeca7812a Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 28 Jan 2026 17:15:42 +0000 Subject: [PATCH 0151/1513] Add basic layout test This is testing a new way to do the main site layout that I think will be a better fit for where I want the UI to go with a collapsable interface. --- sync/dash.go | 25 ++++-- sync/page.go | 2 +- sync/routes.go | 1 + sync/template/authenticated.html | 114 +++++++++++++++++++++++++- sync/template/components/icons.html | 60 ++++++++++++++ sync/template/components/sidebar.html | 52 ++++++++++++ sync/template/layout-test.html | 50 +++++++++++ 7 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 sync/template/components/icons.html create mode 100644 sync/template/components/sidebar.html create mode 100644 sync/template/layout-test.html diff --git a/sync/dash.go b/sync/dash.go index 54ab9abd..e0b58016 100644 --- a/sync/dash.go +++ b/sync/dash.go @@ -22,12 +22,13 @@ import ( // Authenticated pages var ( - cellT = buildTemplate("cell", "authenticated") - dashboardT = buildTemplate("dashboard", "authenticated") - districtT = buildTemplate("district", "base") - settingsT = buildTemplate("settings", "authenticated") - sourceT = buildTemplate("source", "authenticated") - trapT = buildTemplate("trap", "authenticated") + cellT = buildTemplate("cell", "authenticated") + dashboardT = buildTemplate("dashboard", "authenticated") + districtT = buildTemplate("district", "base") + layoutTestT = buildTemplate("layout-test", "authenticated") + settingsT = buildTemplate("settings", "authenticated") + sourceT = buildTemplate("source", "authenticated") + trapT = buildTemplate("trap", "authenticated") ) type Config struct { @@ -71,6 +72,9 @@ type ContextDashboard struct { User User } +type ContentLayoutTest struct { + User User +} type ContextDistrict struct { MapboxToken string } @@ -96,6 +100,15 @@ func getDistrict(w http.ResponseWriter, r *http.Request) { htmlpage.RenderOrError(w, districtT, &context) } +func getLayoutTest(w http.ResponseWriter, r *http.Request, u *models.User) { + userContent, err := contentForUser(r.Context(), u) + if err != nil { + respondError(w, "Failed to get user", err, http.StatusInternalServerError) + return + } + htmlpage.RenderOrError(w, layoutTestT, &ContentLayoutTest{User: userContent}) +} + func getRoot(w http.ResponseWriter, r *http.Request) { user, err := auth.GetAuthenticatedUser(r) if err != nil { diff --git a/sync/page.go b/sync/page.go index 1738e5b7..291c5e05 100644 --- a/sync/page.go +++ b/sync/page.go @@ -12,7 +12,7 @@ import ( //go:embed template/* var embeddedFiles embed.FS -var components = [...]string{"header", "map"} +var components = [...]string{"header", "icons", "map", "sidebar"} func buildTemplate(files ...string) *htmlpage.BuiltTemplate { subdir := "sync" diff --git a/sync/routes.go b/sync/routes.go index feba55ce..abb2be6b 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -60,6 +60,7 @@ func Router() chi.Router { // Authenticated endpoints r.Route("/api", api.AddRoutes) r.Method("GET", "/cell/{cell}", auth.NewEnsureAuth(getCellDetails)) + r.Method("GET", "/layout-test", auth.NewEnsureAuth(getLayoutTest)) r.Method("GET", "/settings", auth.NewEnsureAuth(getSettings)) r.Method("GET", "/signout", auth.NewEnsureAuth(getSignout)) r.Method("GET", "/source/{globalid}", auth.NewEnsureAuth(getSource)) diff --git a/sync/template/authenticated.html b/sync/template/authenticated.html index 0ffb64e6..4fc97cc4 100644 --- a/sync/template/authenticated.html +++ b/sync/template/authenticated.html @@ -8,15 +8,123 @@ + + + {{block "extraheader" .}} {{end}} -{{if .User}} - {{template "header" .User}} -{{end}} +{{template "icons"}} +
+ {{if .User}} + {{template "sidebar" .User}} + {{end}} +
+ {{template "content" .}} + diff --git a/sync/template/components/icons.html b/sync/template/components/icons.html new file mode 100644 index 00000000..424e1ccc --- /dev/null +++ b/sync/template/components/icons.html @@ -0,0 +1,60 @@ +{{define "icons"}} + + + Bootstrap + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{{end}} diff --git a/sync/template/components/sidebar.html b/sync/template/components/sidebar.html new file mode 100644 index 00000000..2786092a --- /dev/null +++ b/sync/template/components/sidebar.html @@ -0,0 +1,52 @@ +{{define "sidebar"}} + +{{end}} diff --git a/sync/template/layout-test.html b/sync/template/layout-test.html new file mode 100644 index 00000000..9abc1750 --- /dev/null +++ b/sync/template/layout-test.html @@ -0,0 +1,50 @@ +{{template "authenticated.html" . }} +{{define "title"}}Layout Test{{end}} +{{define "extraheader"}} +{{end}} +{{define "content"}} + +
+ + +
+
+
+
+
+
Welcome to the Dashboard
+

This is an example of a Bootstrap layout with a collapsible sidebar.

+

The sidebar can be toggled using the button in the navigation bar. When the sidebar collapses, only the icons remain visible.

+
+
+
+
+ +
+
+
+
+
Card 1
+

Some example content for the first card.

+
+
+
+
+
+
+
Card 2
+

Some example content for the second card.

+
+
+
+
+
+
+{{end}} From 5d4a7a4155c10a8eec6d23bfadfd340e5a5d9e97 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 28 Jan 2026 22:25:02 +0000 Subject: [PATCH 0152/1513] Add customized CSS theme for bootstrap --- .gitignore | 2 + .gitmodules | 3 + README.md | 10 +++ bootstrap | 1 + flake.nix | 2 + htmlpage/static/css/placeholder | 0 scss/custom.scss | 51 ++++++++++++ scss/sidebar.scss | 90 +++++++++++++++++++++ sync/template/authenticated.html | 100 +---------------------- sync/template/components/sidebar.html | 100 +++++++++++------------ sync/template/layout-test.html | 111 ++++++++++++++++---------- 11 files changed, 279 insertions(+), 191 deletions(-) create mode 160000 bootstrap create mode 100644 htmlpage/static/css/placeholder create mode 100644 scss/custom.scss create mode 100644 scss/sidebar.scss diff --git a/.gitignore b/.gitignore index 32ca81cf..a4db107f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ nidus-sync +htmlpage/static/css/bootstrap.css +scss/.sass-cache/ tmp/ diff --git a/.gitmodules b/.gitmodules index dc0c25cc..d33f3b73 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "go-geojson2h3"] path = go-geojson2h3 url = git@github.com:Gleipnir-Technology/go-geojson2h3.git +[submodule "bootstrap"] + path = bootstrap + url = https://github.com/twbs/bootstrap.git diff --git a/README.md b/README.md index 936e7461..27749fc2 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,16 @@ nix develop go build . ``` +## Building Custom Theme + +We're using a customized Bootstrap theme for this site. You'll need to build the SCSS into CSS: + +``` +nix develop +cd scss +scss custom.scss > ../htmlpage/static/css/bootstrap.css +``` + ## Running You'll need a number of environment variables for configuring things; diff --git a/bootstrap b/bootstrap new file mode 160000 index 00000000..25aa8cc0 --- /dev/null +++ b/bootstrap @@ -0,0 +1 @@ +Subproject commit 25aa8cc0b32f0d1a54be575347e6d84b70b1acd7 diff --git a/flake.nix b/flake.nix index 12fd6c72..fba2b1fa 100644 --- a/flake.nix +++ b/flake.nix @@ -20,10 +20,12 @@ devShells.default = pkgs.mkShell { buildInputs = [ pkgs.air + pkgs.autoprefixer pkgs.go pkgs.goose pkgs.gotools pkgs.lefthook + pkgs.sass ]; }; } diff --git a/htmlpage/static/css/placeholder b/htmlpage/static/css/placeholder new file mode 100644 index 00000000..e69de29b diff --git a/scss/custom.scss b/scss/custom.scss new file mode 100644 index 00000000..733d7c9b --- /dev/null +++ b/scss/custom.scss @@ -0,0 +1,51 @@ +// Custom.scss +// Option B: Include parts of Bootstrap + +// 1. Include functions first (so you can manipulate colors, SVGs, calc, etc) +@import "../bootstrap/scss/functions"; + +// 2. Include any default variable overrides here +$primary: #F76436; +$secondary: #3C552D; +$success: #8BAE67; +$danger: #FFC01B; +$info: #D7B26D; + + +// 3. Include remainder of required Bootstrap stylesheets +@import "../bootstrap/scss/variables"; + +// 4. Include any default map overrides here + +// 5. Include remainder of required parts +@import "../bootstrap/scss/maps"; +@import "../bootstrap/scss/mixins"; +@import "../bootstrap/scss/root"; + +// 6. Optionally include any other parts as needed +@import "../bootstrap/scss/utilities"; +@import "../bootstrap/scss/reboot"; +@import "../bootstrap/scss/type"; +@import "../bootstrap/scss/images"; +@import "../bootstrap/scss/containers"; +@import "../bootstrap/scss/grid"; +@import "../bootstrap/scss/helpers"; + +// 7. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss` +@import "../bootstrap/scss/utilities/api"; + +// 8. Add additional custom code here +$custom-colors: ( + "color1": $primary, + "color2": $danger, + "color3": #8C552D, + "color4": $success, + "color5": $info, +); +$off-white: #F8F9FA; +$off-black: #495057; + +@import "./sidebar.scss"; + +// Merge the maps +$theme-colors: map-merge($theme-colors, $custom-colors); diff --git a/scss/sidebar.scss b/scss/sidebar.scss new file mode 100644 index 00000000..858c04f7 --- /dev/null +++ b/scss/sidebar.scss @@ -0,0 +1,90 @@ +#sidebar { + background-color: $off-white; + min-height: 100vh; + transition: all 0.3s; + width: 250px; + position: fixed; + z-index: 1000; + padding: 20px; +} + +#sidebar.collapsed { + width: 70px; + padding: 20px 10px; +} + +#content { + transition: all 0.3s; + margin-left: 250px; + width: calc(100% - 250px); +} + +#content.expanded { + margin-left: 70px; + width: calc(100% - 70px); +} + +.sidebar-header { + padding-bottom: 20px; + border-bottom: 1px solid #dee2e6; + margin-bottom: 20px; + overflow: hidden; + white-space: nowrap; +} + +.sidebar-menu { + list-style: none; + padding: 0; +} + +.sidebar-menu li { + padding: 10px 0; +} + +.sidebar-menu li a { + text-decoration: none; + color: $off-black; + display: flex; + align-items: center; + overflow: hidden; + white-space: nowrap; +} + +.sidebar-menu li a:hover { + color: $primary; +} + +.sidebar-menu .menu-icon { + font-size: 1.2rem; + min-width: 30px; + display: flex; + justify-content: center; +} + +.sidebar-menu .menu-text { + transition: opacity 0.3s; +} + +#sidebar.collapsed .menu-text { + opacity: 0; + visibility: hidden; + width: 0; +} + +#sidebar.collapsed .sidebar-header h4 { + opacity: 0; + visibility: hidden; +} + +#sidebar.collapsed .sidebar-menu .menu-icon { + min-width: 100%; + font-size: 1.5rem; +} + +#sidebarToggle i { + transition: transform 0.3s; +} + +#sidebar.collapsed + #content #sidebarToggle i { + transform: rotate(180deg); +} diff --git a/sync/template/authenticated.html b/sync/template/authenticated.html index 4fc97cc4..ba12bf54 100644 --- a/sync/template/authenticated.html +++ b/sync/template/authenticated.html @@ -5,109 +5,13 @@ {{template "title" .}} - Nidus Sync - + - + - {{block "extraheader" .}} {{end}} diff --git a/sync/template/components/sidebar.html b/sync/template/components/sidebar.html index 2786092a..be4f03b4 100644 --- a/sync/template/components/sidebar.html +++ b/sync/template/components/sidebar.html @@ -1,52 +1,52 @@ {{define "sidebar"}} - + {{end}} diff --git a/sync/template/layout-test.html b/sync/template/layout-test.html index 9abc1750..7a2a135d 100644 --- a/sync/template/layout-test.html +++ b/sync/template/layout-test.html @@ -4,47 +4,72 @@ {{end}} {{define "content"}} -
- - -
-
-
-
-
-
Welcome to the Dashboard
-

This is an example of a Bootstrap layout with a collapsible sidebar.

-

The sidebar can be toggled using the button in the navigation bar. When the sidebar collapses, only the icons remain visible.

-
-
-
-
- -
-
-
-
-
Card 1
-

Some example content for the first card.

-
-
-
-
-
-
-
Card 2
-

Some example content for the second card.

-
-
-
-
-
-
+
+ + +
+
+
+
+
+
Welcome to the Dashboard
+

This is an example of a Bootstrap layout with a collapsible sidebar.

+

The sidebar can be toggled using the button in the navigation bar. When the sidebar collapses, only the icons remain visible.

+
+
+
+
+ +
+
+
+
+
Card 1
+

Some example content for the first card.

+
+
+
+
+
+
+
Card 2
+

Some example content for the second card.

+
+
+
+
+
+
+
+
Primary
+
Primary-100
+
Primary-200
+
Primary-300
+
+
+
Secondary
+
+
+
Success
+
+
+
+
+
Danger
+
+
+
Warning
+
+
+
Info
+
+
+
{{end}} From 20eda6a1d83ecfa0c00d82d669a5e448a8deb44b Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 28 Jan 2026 22:33:32 +0000 Subject: [PATCH 0153/1513] Make logo change with sidebar collapse --- scss/sidebar.scss | 24 +++++++++++++++++++++++- sync/template/components/sidebar.html | 4 +++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/scss/sidebar.scss b/scss/sidebar.scss index 858c04f7..ec0b015f 100644 --- a/scss/sidebar.scss +++ b/scss/sidebar.scss @@ -1,3 +1,16 @@ +.logo-container { + display: flex; + justify-content: center; + align-items: center; + transition: all 0.3s ease; +} + +.logo { + max-width: 100%; + height: auto; + transition: all 0.3s ease; +} + #sidebar { background-color: $off-white; min-height: 100vh; @@ -12,7 +25,14 @@ width: 70px; padding: 20px 10px; } +/* Logo style when sidebar is collapsed */ +#sidebar.collapsed .logo-container { + width: 100%; +} +#sidebar.collapsed .logo-img { + max-width: 40px; /* smaller size for collapsed state */ +} #content { transition: all 0.3s; margin-left: 250px; @@ -26,10 +46,12 @@ .sidebar-header { padding-bottom: 20px; - border-bottom: 1px solid #dee2e6; + border-bottom: 1px solid $off-black; margin-bottom: 20px; overflow: hidden; white-space: nowrap; + display: flex; + justify-content: center; /* Center for the logo */ } .sidebar-menu { diff --git a/sync/template/components/sidebar.html b/sync/template/components/sidebar.html index be4f03b4..11314973 100644 --- a/sync/template/components/sidebar.html +++ b/sync/template/components/sidebar.html @@ -1,7 +1,9 @@ {{define "sidebar"}} From 38d492e9da015b0fdbde2000287b626a8504ed7d Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 15:08:11 +0000 Subject: [PATCH 0183/1513] Rework custom bootstrap theme to include all of bootstrap I already had most of it anyway. This also fixes our buttons to have the correct contrast. --- scss/custom.scss | 86 ++++++++++++---------------------- sync/template/layout-test.html | 35 +++++++++----- 2 files changed, 53 insertions(+), 68 deletions(-) diff --git a/scss/custom.scss b/scss/custom.scss index a38ce76d..4bdaa478 100644 --- a/scss/custom.scss +++ b/scss/custom.scss @@ -1,65 +1,39 @@ -// Custom.scss -// Option B: Include parts of Bootstrap - -// 1. Include functions first (so you can manipulate colors, SVGs, calc, etc) -@import "./bootstrap/scss/functions"; - -// 2. Include any default variable overrides here +// 1. Include specific theme variables $primary: #F76436; $secondary: #3C552D; $success: #8BAE67; -$danger: #FFC01B; +$warning: #FFC01B; +$danger: #6b2737; $info: #D7B26D; - -// 3. Include remainder of required Bootstrap stylesheets -@import "./bootstrap/scss/variables"; - -// 4. Include any default map overrides here - -// 5. Include remainder of required parts -@import "./bootstrap/scss/mixins/border-radius"; -@import "./bootstrap/scss/mixins/box-shadow"; -@import "./bootstrap/scss/mixins/breakpoints"; -@import "./bootstrap/scss/mixins/buttons"; -@import "./bootstrap/scss/mixins/color-mode"; -@import "./bootstrap/scss/mixins/forms"; -@import "./bootstrap/scss/mixins/gradients"; -@import "./bootstrap/scss/mixins/transition"; -@import "./bootstrap/scss/vendor/rfs"; -@import "./bootstrap/scss/alert"; -@import "./bootstrap/scss/buttons"; -@import "./bootstrap/scss/card"; -@import "./bootstrap/scss/forms"; -@import "./bootstrap/scss/maps"; -@import "./bootstrap/scss/mixins"; -@import "./bootstrap/scss/root"; -@import "./bootstrap/scss/transitions"; - -// 6. Optionally include any other parts as needed -@import "./bootstrap/scss/utilities"; -@import "./bootstrap/scss/reboot"; -@import "./bootstrap/scss/type"; -@import "./bootstrap/scss/images"; -@import "./bootstrap/scss/containers"; -@import "./bootstrap/scss/grid"; -@import "./bootstrap/scss/helpers"; - -// 7. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss` -@import "./bootstrap/scss/utilities/api"; - -// 8. Add additional custom code here -$custom-colors: ( - "color1": $primary, - "color2": $danger, - "color3": #8C552D, - "color4": $success, - "color5": $info, -); $off-white: #F8F9FA; $off-black: #495057; -@import "./sidebar.scss"; +// 2. Configure color contrast +$color-contrast-dark: #000; +$color-contrast-light: #fff; +$min-contrast-ratio: 2.0; -// Merge the maps -$theme-colors: map-merge($theme-colors, $custom-colors); +$custom-colors: ( + "color1": $primary, + "color2": $secondary, + "color3": $success, + "color4": $danger, + "color5": $warning, + "color6": $info, +); +$theme-colors: map-merge( + ( + "primary": $primary, + "secondary": $secondary, + "success": $success, + "danger": $danger, + "warning": $warning, + "info": $info + ), + $custom-colors +); + +@import "./bootstrap/scss/bootstrap"; + +@import "./sidebar.scss"; diff --git a/sync/template/layout-test.html b/sync/template/layout-test.html index b82f295a..f5894c89 100644 --- a/sync/template/layout-test.html +++ b/sync/template/layout-test.html @@ -1,24 +1,35 @@ {{template "authenticated.html" . }} {{define "title"}}Layout Test{{end}} {{define "extraheader"}} + {{end}} {{define "content"}}
-
-
-
-
-
Welcome to the Dashboard
-

This is an example of a Bootstrap layout with a collapsible sidebar.

-

The sidebar can be toggled using the button in the navigation bar. When the sidebar collapses, only the icons remain visible.

-
-
-
-
-
+
+ + + + + + + + + + +
+
From db9d4e05b718bf3ef7ece4c55b427832eec72d23 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 15:13:38 +0000 Subject: [PATCH 0184/1513] Add missing dark/light variables Fixes things like "bg-dark", "bg-light" --- scss/custom.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scss/custom.scss b/scss/custom.scss index 4bdaa478..b69de586 100644 --- a/scss/custom.scss +++ b/scss/custom.scss @@ -5,6 +5,8 @@ $success: #8BAE67; $warning: #FFC01B; $danger: #6b2737; $info: #D7B26D; +$dark: #3b1002; +$light: #fde1d8; $off-white: #F8F9FA; $off-black: #495057; @@ -29,7 +31,9 @@ $theme-colors: map-merge( "success": $success, "danger": $danger, "warning": $warning, - "info": $info + "info": $info, + "dark": $dark, + "light": $light ), $custom-colors ); From 1dea676ef9d2bcd3c5e87a077d62c7b4f83e67d7 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 15:22:24 +0000 Subject: [PATCH 0185/1513] Add banner to main RMO mock. --- htmlpage/static/img/rmo/banner.jpg | Bin 0 -> 133315 bytes rmo/template/mock/root.html | 22 +++++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 htmlpage/static/img/rmo/banner.jpg diff --git a/htmlpage/static/img/rmo/banner.jpg b/htmlpage/static/img/rmo/banner.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a01f736726e65cb500464d60ef4b18160ad3f223 GIT binary patch literal 133315 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 z zrB~V~o^+(DZAreb)H@^nOvv#KkLq^#Wj=Vw`>1u=%iMKOs^b&SYqGtbm$X}-JO3o# zncJ^EO1^b6U#Yp^y2k!#meq^rHIzRvO3E+Izn9s%^ug{&VdgBEMahxD zGIInt*4())nZ(-ofoZLJC`-4%lw`@{g}Bkea4#Hx4O4ve#|s`6L-_YB7xyQ>xTkj|T5s{v zNx@C^8`9?1td2Zcc1JOwvAJ+kWxYdYe&U*^epe1#Kj}4UsN!c1o5JS ze}6^v-Ok!APtE5ugcKJav6;5OcHgxdd=dT=!mYPlzPEPIk;c>7`~0UG1~S{kUOBMx zUz417qa$~w*xlG|vmU>AyEk%=oMHl>-aA*RrUbJV@^P;jP+=bF<>8ox| zd0}8R*?*3YpkvY*`vo)Z%Y|N6m~r%G#Pes#8^RC13X=^zwdTq8CZ+dlpByu{nqKcW z_3GMFzBenn^^_+a_11VHB^Ppj!2#yg*=M}o-VFYDdZKS>{*iUJz8urt%DBq##N(Mu zCokI5<|DCq=1G=$^M9>&KDt}wZ2spy*-GgHA!ai2WWhM%ixP)SJ}~yyw&PrIed4NQ!(uz#Lcisys#J?ve(_14>n0(OEA8P8n8q zPxV=HD%Nf8-#uR!R2)5YCsVIEiYHsP_REC`53^~ukqqA-Oj>SVu=@D}y^Tu`E0;Uu zxk;p730XGbI`d8|pN^PcFa6egy`HZe?yJtQXIt{TM=z#+5mI{lZKdh?-vQGD*97_I z-#K!+Wr7;x!B;h}Yd;z0ojj1g&O^bsbZPMwcd7huTVrm_?_s`v)c@82hR)fo=l%sO z__;l2$CiZTkVQUqS6!$7n3}f9H?uBKU0im{qjh|RO2_BlDqK>3B->(fmGUn4wUv`w zH}BAU{KCMjENsk_?n=9V*N$APYGQawM{rc+aTR)a+?eRVE>9YLC zF5e@+9gqIFeB_Sb({)h-E~!mDY{zOFY*wpYxpOi@sc1?V*TN6anb-oZQ9BJ1ECoVGq^W?i{5%ors`yFzL)zEt{3y}`D9F2 zW?7Kik+<4=$NQ7Dt@l>$2sw5xcz@B)3ePXUPJjQsdcKv0bdI~S-wyRWi|@Qg^3Bid zw%xt*Gl~6q%D498O1r)DmVG;#_ViH`tMAk|C6~(TRF=$%-!!MV+3@GvjRMcJTRtAC z_pz)g^txxn=3CCkdh4F>@y2b(OG`ptZJW+w@Ot4+gMU0>?%VfH-nPNKrc?%&RJ4|?~W)mphAvproc!rJxF^5oXOPj_$MQ4Pv=iLLU9E_yp( zGu$`-rsttK<(FI~?@MQ0|KqmfmKdt0GdE@7kz_&&^klVh(j2EB zHtWQs^OsFAI2X!c@#Dg_-7@Y9V&9z4^r_m-OMkeB$=bMe=N{QNOLz==-mS{k*O{p{ zvv8fl_w7YKcV_$uKk2UMdE?o%Wp{M8rsamFC6GUec>Yp3knu4_GFxL3IP=aeVD z#U``%yS%x%_^q49CH3+ttsDFK*Gl?NTfL#v`}-vRMg2$qJ-qxlwtI8pvNtgbZ!S3+ z-2Z%5wMboCisSlCnZN#XOW&QG^UdnQe};sVe==WW8+^kjujZe$^@Zc7V{VRzPsIFZ z*kDurNx8eb@|@25LYbCN;pZ>zzu)|RdA#P_`4?Uu*S&t|N8bm=uRB%R@4uPfTKqQQ zdd!=C(dI3C=S^?FU#s#_{p>r(XD8f*9ACeFroG!zG;?=)S*=!j*VBT`kleDT7gt#5 z72gd0v!=%QcSrw^()@kFZ56)DzYG3xTrHh?SpC7>&6_vPF6(+H8umm|_PTd}h3V#> zzW;K==HGGu$hIz+C11y8PQp5ediZFyiU|5J|ijenfKoqzvhb*Gs2j@PlfpRzRG zR@Y?de1Cy#14|6_WxWCTUd{fE! zApab8hL87x9;Ru0zHCmrR!*~UEm9B8JZ>A|1O z-IljrzLT!t`En6m6nXtv3d;#lve#+xsG)RUH%-OTuGtIw&^`*s&Ux;p<{y4|Dfskbu44Ic34 zR{nmyqGozoZ$&%b(q9()>vRi!lcjfv-uxH*pCRS- z{#z6N{fTja$GvV8kFV9(6oF&U~E=<+Vv!ZKT zT|HmTm%V24>em-41(%CoJT~#o+ITVs6F1x+8=#I1ala`*;se<7?$(9Km=eqKX zZ~eQl_RPV235AJ=8df$i%kZa7S@*Z=;Fo(<{mb2WZoNI9UVK?$$JWUOzhADIdw7cr z)8T|7=F;yM;zT=U+8gKXi`%N*y?XZRY4aZbKDorQ+(PoeaWCfHI~{L(58=|A76sc+?8Rqg3| zN^bA%3YFu_l_Tebb#MOTnDhSeBnK%mUYS*uB85v=nu~6p-2PE|wx{KhC;Xw-3xBL+ z+i}|R?EQ0cQ72E8M{;Bz(uzD=+3f19SQ*dYul3z?J>&5e>(-a9cXRcSxqcvPeUm{# zcDGZ>Ny80WlS=1r>QB3r)g5`DgKx)rmFa4eDvIZ&t1fro*gDzn*H(|C+L0Z%x|;&7 zwqI88dM(F0CEu`jWpeY!Wz(5W_PI(fx@dDn>+qcR`=;O4NNsmmHKSt1XAKpnFO#xf z_?fL@w_SBq#kBB{)9d1J?*_ByAqmgc6!|)vPP?INZTkP(fi1rKq$hs3-Z9BtVY-C! z+^EoJ2Rr>M&+b_j@B2vTIdj^Vl@Wbmk3*w@K60W0A$D zrqH&ydYNw|ei6cj3FN*Vd*QZz|7zF_r**rwObhsIAtjz-V!NeDskG>6mz|E! zgw%t~oY@OHSiO{3*0elzb1?nrv}Wy`6*jB#1n(R6Bo}F|d?F`McH&Oe^_I!G#jysg zzXh*#3JY-_o;R1*>D&Ds1q&XBJBA)|Z1=q7`s8j#W6dTv<*AMlKj*K>a2F`)?sZb# zaPPS5DiIKQ9~#73z8CzqKpNUisPus}n|yR{mSxEp56j_MgE@T6yESs&I)1 zg+AQ}=DuFXc9HE;qM*pNryCyU-x@>VO>s2}2cW8|?gdQXF4=O*!q zc}=@+B_20YciIu>utPTMWaMd1!v;P#n-xFu9_?<5IN|Y5Cb`bHD}L>|g^CeOiwnxu zE3_s}l>8p&ESI+J8Pn~XXHSVa_uP80{(`Vt-@y}owey6W1z+Yi$IqW}Xwt@xxeRY# z&-%-0tyQMxUbs@c%`ny0Vf&qMwnq~W=ly-VQhJZytqUxF_bf(c{flUx z{amjZRvZtkJ+V8z=yd0@kAb^xUf9vP`OdLRri}+@q+Z?m@{?#e?=FtbHp_$LbL0)< z;y-l%V?Xj_`()dvv)|a>>EGG*Xm$2(Prc4#x=STWUrBs6X})RE;`hla<}Sa;?U?}7MenV7bLshJgGlX{o@;^x6(kQ- zIAt9_w#oU)Jkg!voJVDfPkmQrw_m<|+bmP9gaayUuS&fxt~!=2_bbS?nk%@+k ze-}Qpz2ZBiXeN`G>XtQx2S1 zXuV;#HOuoAKQC=9i#piS*1<5jFr}*6)#!oBNrRmy_C(~VL{8nB(h_*#+V&^svfJL~ zZaTBnXmW!5t=!0W%BuUTq(ZdUr0)xyBXzZAmeu-*R^Ei^l#eh()7#k zV9~Sl%VJkrWbuUYYP)_~@viq!WM$nE=8x~PMOIDHi(wC9JGRYUTao)it-JhLKYsrg zti?B;?Fw>MTN3AEv!c3MJ)@G1cTdCiO;#G)-2OADh97w#uxrNj0Gp4OmL?k=mRWk^ zb;<>XMOO~nR9btUuA8?-^p2pzD$z~Hiw>_?u=U35leKY_g2uIx*wb zd0W^0RC`o-v#|01(1If7s*icMuDR^!crjDLriiyA z)iTKTN|mXs?;+{Q%XiNb4P)QZRLs5P&(gB8OP;|!d!!DpXnJ`3s={As^ZgdTwWpsy z94K;JkXO<^eBrv|H~nsXSaNsQk(fy`#^Kv)>l&t`~0TN zJo6!P{*-eU*dD!37W`#xa@gGOW`hCSq8Y3T|V*i=cRS+JKq=VEd8~`P3XWi<^FUW>?44iP8yD7i!k$-I1*M zSL8m;y|CvpgWt(u&w_cMIMom8Tw|ZL^T@R81@iMeT+BK3rgCm(?`IElExX5lB-c9j z=JKvLwc0DU@9*{FI>GQ!&oVtd>hX40xg(l)xAJ{FKEvX+$@dL4@t?SRC6~{rIrP)T zyxk&XBL79t{?4=9DeGGfzmX|$n9@~+&OPvQm|n2$`NG`&VusGQR$9$dGmk6g&smi7mDNF}Wb+h@7Q>r&{COPP zOBEcJ&(@I5_;)68$A1P+J^qU6Wp6xf4tp)EJ9A9fQusIMZxcQ@LSIQjWIek~RKY4MA>t(%`Uhw9L1_za1*|0r*b26st@sT7B zZ*K+0{)N{6v|j&b(3;3uZPan(@$ok;2p?88>Z9`qHmwS>5{OkypJ!A+7JOW97-aj?x>?J3sZxabB`od(Xrv z7H+YB+za@Z##bxMIWqV3@i{tg&Ti@9ca2M$T+L|Vdq8qobBFLm)o7il^`(|^&t7&o zzgev*aqh1xqfL02(AG7lxCERw+>P|#bUN&gwWlVJ_xC!HlcyMaoBCI~xayRfWoH`8 zv`fkE-4QW=^)vpc%bpp+g$&Z=Tqm0ilg)gpmF;V9$qBTUoRqo9KA|>ATKdr6kj-0^Qdd&n-gGgiRn z*H@jc9qCtgiTmxTob~MFmW@+R$3Ch&)40xiw=cWxgYD7QD^o4@To?c2SN(wbr18xD zRXfkkZ&)3+NJG`HDmRI(PnUg99?sPaAC^7jkr)8&0b$cL9ZMg1OI%l1#LP>Q}GB~ZD_L*6!UWB-OrYb+8bE65eP6->%yd9rKy$>~zw z=fv%K%Z_|A+UTOLa#&d`?C2~yi!AyKHaSmP=*Dnr2m2e5-<#{zZIl~YcJ7&TKgNMsXHvsNMc=P2 zC+}2Fc9s@C9l_CY?w9Tl)df46ji*N?-}omqN4#z`nfR9zVt}a#Pq&rfl3{p*CxwxOnLQspzU$3-9@(-hM)5`~By|e-utAB(iKn`jWL5mOM6%zf^d-Ns52g z(r?Q?Wak>bl9YY$dHLj;H=Y$Z6Diakw0b2& z!J|x@7Z;1w+q6r1a}NsbWKgr5yf5NsXuN&jKk;pq(=4JQE~+Fp-Kz0_{9@<2-XnH? zZ=X-FUHjN|xmA$s^|w6L4(k;iB5Ni`YPrgOee5kHaLHXN^GdzTkq0%}XKrYpvD`jy zhVrf*Wv48g^F3QWKl%Gke)Gi6e@iy$)VfW$Yim5~;K!8Sis`~L*_)pmZE8L(b-VCo z^gH8YJId4FC_mgL753b}JeTR6lEkt+03Y8{Jo-D z)Y12=#+9vB+t>@EZmpcob|v-R>EkR-hg)tgKBaP1Bz23C@Ym^=XDqqsGe^1e&WYnL z;VG=IOnzREd@MUUx%C4qskNd02k)hmFT9dc=O{@+=P9`t)w| zEas4Z6 z@w>|(v-_;ew(BZHhx#1Oy~_UlKZ8xx#RGXq9nb2qt@U<$(5jxf>JoeF=Gdp{Yu`-s z_KcAin-}-$*Qs}*y>gFo&NwEjnOiSvs5`aoTE?SFg>z3H^y_r-zJ1p9=#fU(6v5PH zo(Vh+JylPF)=U@Qdb4DfSIU7y(T_c1uXiYYtYdhlrkJM`8L-@T%gNTSw?&V>e;b>+ zODXP`MxW@3yRCWEoVQL#dt7fUv~G2nr>)&?5hODyDXCJxzx~a!JJlP+=6FTw>Ezj^ znjD!K?6%{^yoHWIJD0BxD!U{ZHIelI+YXVrQccbC{gyNuUD|m+<$1KljMC&48=i#x zXAq6m-el&t>292X?Cn;M8KrkN*DY0la9{Ow%7eS{-&ana(rYq#@(-Wm@~Ig+u1i0> zJLlo+drLvNr8EsSPJ2a^A47 zTz5P#q|8i>f2xeB$ArpqxeM#86f1HkrZOBZ@Z_u88Iu@M5`R3lN$2E^B_|lR^GGr$ z80TN=+^{8SmD}2j+}Y~9mS5I}8*~Om=T`MhTyZ60lDEu-;+5yC_~zs=ys&9lxHd2D zF6$<@OB&C6zM7OwVC{|A7Q1)JAE{|k&n!-EGVl3v(KE03Kn~u-&+{d`*z_(?>W;Xj@=Hvcjn{G9kQ>sZ_QP>T*yD;#pzd8 z!ME+MNd_*xD`c?$tatc=xI=8obFSQa=l;OP<(8Le^xO{RS@RS3DeaxVMeN$9@}k>2 zTaO*7Rtekm?s#L_s-y7%lbH{iq;M*$zncH3SNG52{bze;$95|G>N!!me!ALy(XM;V ztJsqkSh`ugZPMY7I&HQp#WgPbXiw9|OW(S3%=x=+RE*2J8LG^oyvvGlbrZ?fL-EI`fsWE z-yetVf6|k@sr`57-|NV*F8O_?7Y$B(jiTinM$49rxm%S^qx%Kz|_{s81rb} zH~YSuAN+Lm@^ydTm?0v>dr#+csZ8IUm+Y?YZ95(cv%K^5Oin31-b|C6)Q{tF)+lK=cs zG59TCW#8cKZY0O&qQJX$$>*glX=3)RF){jrTdHfmKMHww=(FyM1g_V%`ztPQntt@> zycg>8)_t`M*u%V`M!n&bMeCVo&z=cHrcK~@)Sr7p&+p%oYxYmMw;6ow3~np9c0HLn zds^bDNVdcN&%@uFM3wC1GG*SpLcx^t{nM-s+tuQQj-HjT+#wyxbfGfPtH6A{GuKn2 zs2gsFWKvmGm$4N(dPteE`f*sYY9&D^;U*B^c-qAxPD|JjF1Yj1{U#dC@nlqz~A zlx1%^W)bawHbEd-&gJBCC;9D1vb$dy@*2KbJoEF~@a*3{e~KO^wTNtJnD^4(EHCQz zS8byc{$9~pE3|A9Uzj-8T=AW`Y&M_XOcr^QdjH9}+phJD)BW@O!pX%-Q!jl6N9?52H6wYNmpY$SL zgLnBR8^=8@@%tCAQ%I4x@%!}`ahZqS$1Gz0SS~Tt`DLsiwQ=D!zexG!h~+;OwxnIz z<9XTUcnQZp`EavYrt$$|oe~Z+B~J!zi~6$il5X{aN9xK`SWic2-oN?#XxOR4%iL$I zTUO`3w*N~={axiBcawjo-^}=JSenaHu3Mnqb0T6{Pr}P%j9JVpLK3EP$Y!>^6}k6D zw`AU;&6exFf2#kj{^9(s{NEp6x3J|^P5J$LPZ_)YR6fZ$)x4*_Exx$TZ{{n9+x)Hl zvQPDxw^oN`)it`--t(NF>B=N*v|Ih8`I|NO?}W@hoV?Eacu~cD!GDjI{rz$Ex}6uN z?ev@p{^wGj{t{W|dpx!9*p7xP>~BKu?9Vba48It1X-j{5= z>F)REyA{`IY~1-tUh3_C2F-=S8@Fq*3C0PC7F|3o5kn`R?)Aa80WmZk+PQTdv zxNDzDfP;GA>%P!?d4)gSrj-2Y%KsX3ovmBuXI1}|^%ugnHGSXo;ilMy?Pl7X1;$G} zIeZ>pHhnwm&jRV`+g2B)ypKt4T##peIG~@uuD#CT)raSgT|M|BY7;!{{#?6v|H&>6 z^{&JtbH4j+%6xZ#Wii{qkG}$qYUW1UT=LM5U{6cfd&A;Naevl?wHw~*HAx#DQL~YB zzO<#M>-j~qX_havjgO^H^Io*3hIwkK+=XvydZLF@_V>MAnxbXBsnPMu!OB;r=XbRB zSC#f>1gyMVc0Nk0L#1@lw#8b(7CfD=JWP`1YWde%Y~A_uP3)a^a}CS6TN|(6(@g2U zyV3bfNSK>@xO02()cmN@H^=V%_WCT_aC?hVo|2Z|70nx$6F%NMd~X4_(p#0fxW$*I zooe+wKR0c5zsrr*TWeX^&6Uhn zIlo(;*=pSWK(T;^9n_vsZTS1b>|ob%zuvCWDINqx?9EhNqgX66^OIDGCreyHW!md#7tlrPSR zPPEv>&T&pP?5xmnW!8_@LhUE3xLOL_jhftIO1`e#Al0E<*R*)yRi2A)=k7CIxwS`l z)kE=94uM6cT95gbvtL_L@ny%9+Cd(*jA?h`++S8Pqcw06qHH&s_Pg}1aRc-AYQ7D@3s z^q}ODort^Tf!Iqg)WV`J_|9fl2|X_U_=)M_kJ=kEf_EE~e=1M2$-KBY%;a%Sz_pLN zk9_;g8)&}SZJ8>2gw*B}SG&4aPu?KHeqF`!OiI@+2O&3C<=q=5v0ZA|_)H@7n@wLl?rx7eO^+nw436YJx^QS#8&7+@{-G}m^KMXm&S{=ux7GP| zmp^-dPGD48`P}k_6J?{ft6kf>o(igd;)WT9uEJ?#V#eDN7 z$=z*T$9G?JS? zZ}AU(k=dZ*C(v|l#&P?gqa4cjYOTZJQSmQmGoqu-bKMm9tDca<8(_NCQ%yPYi zS(A$Ip2Z7egm}Iw|JZw7tTHhFz52e6r)%eB%K2zNp0=sLk1eyGzqhyXvvR53lwQ3E zz9-wZJY&4iQW;!br^?~l+Za&BRrze%Emx#{r$S8>Js# zS~7c19ysyy=5U2^)JE8JT_EZh3M`dS@SVYrAiH@f4>~ z^nr-);XB({n6H;E+V?hH`cc65qK}`mO>9h7t5#qFh&o7;1vlA+~!u<*WGt(6@$ z5)YrBVT}9k81S@Q`thkvF^}}V$5&1gcaAd3c(ZA{$uF8-i`TmmgT!RlhDJ%lIHI=&b8Y*ElH(kz4+DA8wqk!+B)yn@2EXqoo_Pj z+=JF>VkwDpx1}mv-1PCDVu5JIoKN?(QiJzRoUA6D2!sL%=Q9j`LlKl^PKbr&#T3;k^5=eAb2HQ|YZiHacs5U8xOn zUmK^Fv$TO(yw8Z^xIy^x9asH}W?d`fs8l^&bEn$yaZZ1*Q$oK`itsG$erXZ!!oI8J z3fqNu_sL|WWIhoTIW|Y@)iJ}6+uUW9Np5<9VNU+4g&VcMT~X=qi&UREvn9<%R^T6> z?N#gQ!2HsDNlEV;9arbA-yqK%UH#GQ*#n8bSEt#86KCz!Gu=1MpKr|yo6h4_%VWNM zI+m=n`JlqwNKf7sc}qQ>xbl5I#c}nd+uXlqD^1paNv(18b9m*OV9=~6ut&ft+g$ol zd56;8J@Yb(?D{feub9u1?AMam;;$HV^A-E9X_66VA`gZp&$3dLy25Xh@-uwiB|T3y zv6VYq7!>0s9@=0qVH(RMf#P#kmp)F~FyXksYP zyH|D_zAk*HyL5(7xbU`fR;+POYbG6yyLT*f=92d!Pvp~V*^eyha8~f{d(@~mnR%t9 zOjgF)YoU6RTpnIL7TPp%;}uK6@IGzZTQRDC-+VVX+}^);;*-m(m-o!_KfEq^+BBC+NAQOf@EefmE-NN@--p10^1jtv)Nx^gg^@wmLUm zvmjPDxF=INao+x16K|7=Z5z+z-1`?A@bENG>tkDUxvlFWTLenEEjUiH&S>@!NjW{I zZ-JDU#QWTJQlD*NxEb&pxm$_d4z#_^y!5tJZN`~G zW?{cZ-m`2+bVY1ezFktbD*4eSiHj1a-U*&Ka3l4~oLSLFc6^t7WTbWP-g%oual_q$ za+{3&S`U5xvMA?Kq`IN!uJmbDSNyq-JPl`EV)f-K!>2WK*Iwr@JXsNt(R8iTyTN_4 z-s6;{lD>3x5szu54>yZ8hH)1$?YJ@L)0L`=xrc-938h*;m=?dUX>-Pz@_;j(F>}1WHUs0VyozQwFbx;jqPXtBp(Bl8L-jVGqB)>r&z$lM)pjpOZ+XGhLBcl4J& z{C-3{i|vw2^qEz!-Yrxw+@SmG>f5_Nc5bOSaph=)kpCiy*a>nSmtP-h}zbg;Hyty0b1wTmOd3@7h@D8Fsh zV{_KrTe7z*K6oBCkw0uj z|BTA@-ej){vsWZBoBeCJ)Xipi==x^0y~;d=Z<-vWOP6g|-m|f~Wx6&);@zSv{_%BR zxA*8J%;YWc-1}XpNk+zRVfBF@bN?<;dUWesbj7jrGI>s$8PBZOdi{sH$NBgUtMk$` zo~2&22yS;jW@P*4Xu)lcyhoj4A)2`bHF~vHf3m#~R!qIlQ)s)SO)Z{jy_@`e+00qh zfj3{txa^y~wne6$PeW9rXe|rF_1^NYi>!kW{MgzqU>?qG`y?f|sMKS3(Up6;c8@(P zN;aRb>s=m|#dDEylEcebCH^Vg9uN4B#ef* z#nnKz=V!Jwi*DTfX6us6>*p`!RNl>Pv$rZq-di^+L9f1M+NSukMovNQNw+$mPu_kv zPW9^E`I*0uY%@1Fn6ARboRwkIOq- zjUCm|xAf#Qr6NB{E((odxw_WPinZWC(CMAC9u?o)=C;VwZfow=Ek!rHwGE7TIswYEEFx1U>~DCM(n-4Aie0s*Z-m6>IGauqH@aPF_ zyR}qd;rXT2$KF@;{%v7;_41u4qvpNM*2y_@Hrl)tI;nDrf7R-TcC&T`TU=eYaAL6j ze3s;OYzMerH?MrSw^8h3h1h-(vnSH8Cb~;bOLbwLF}LB)<&wxXp^*V%6ANBFmab#} zSo*e~bMAVfV>i2E1bpx8O%D7TeC5;fas%h=LW#*Qdz?Lb`_}Y?y}p*Sg}Y&WdYh2kv?HnMaixJyz)xnsWOi_u3Sf zqdx6CnzvkwHi{K-CO$fv{>0v77ti9ua~jH@`ka?>@Ae2g9~^m0bLS0TvnJk?amzk1 zH{UHx@d@Xe_NI4k;x^HQgHk``qhe)x&Acky%+DJ28LZ&;khW=f?R_Or_;``ApJL9$ zKX%`u8GkCLs(suxQ<=w6;pgSm`>q#SI(ep->p16qveh`y^K;YW$c($yO$rDe^|jzL(lb2{{G;~aI*t7GBn4{B4y+5y*vhL`%tI0lZTPd*h$VrieEt3pF<(51M)z-_r zpT6jlUPVuLOC#6qowr~1>r}TUK55jx+|kgx_gzKmq=FS3d2goem{42EZ@+2s?ZP_M z!%vNF#CqpEpV@ip`4@+rI@7Or-kkBYRIj%_r2}b>py8} z_)FsR?`b_(g$wx<%MN9~o|%_!ciQXdmzAH+YenWty;rX0Fv@scni(D*e62b3UG0(y z7K_+6&Dg+ryx?M$p4Z#BKTpp)2Mes4Giz%|Wx#{=9wFRquQXd^iaU>UG)ceiSQy}R zWYVq$>@&EeMGPIUga$b`PF)w15?;C7U~dJZq^M&@bC|m&yLaf1usdO52U~1}?nL=) zzU}ql%*@L=Ie~3%e3BkED^~uPFSOUS@6)I1mf)2^nQ{*NIt@>*_$0OloCtWnv{-mS zr@=OZ$p)$>4+4)=Y~5bDtLbciXwc#JB1~R4o#!6=Df8K<+DyvNFpJ@8)6rdtC3ChP zn(DQl<*f9Z6CUM~h06N3C@X&o`>?Kd>5l0ivsRtYtadT~Gi|r>`R79a z8Kf4Z1*oX{@dmH{vizONjp~k4udh9#ADgmg z_pFx_5~V7ooY-xco?OlC+b8_YGBjsx&)nlP);7I;vQFT#eJ*YZ-4w}n$30l zcA@s}3V}qWu1Bt}lIKpXVOYR%)}`x)N=jlvL$T1lQ_sJ8uJ2y&V0Y`o^%?8+l-F83 zm)O|7bJPBv+uBuo3VcwUqU^Vkk% zES=>kFrmn3-I0K-@)ZpJUK>UCY<$07$m;_`LAYDy+x;08UkbIB3+?7upi|iSgp1(@ z19Ol|fcf7SpMG#@$g|z8o%n0(yq=k@x2;&1W~pcFb}3EoiWA;>tFpc~`1ZaFg{6HL z{``AjxwYYBXH05Wdf>-mWtM9mlP0{3z3203Sxi}K(?9RPzud<~EIt_?TU9Tr8F*Db z;qkn`C)O|bZQLeYxyk5p??>qwXN>Rpnz$FsOtRKgo_5A#>7^Z9XJ2}^9#qeo9n;Vv z`$^koa(Bz5stbo_v91xa-mY&wKQ7eGH`@Bu)Y;cosjV^HaKW5qVd>c`Z{IE5HpN@= zPBHsLxsY3rG@f_nDc&=eu992)(3AhZ&ko%K@1xEIe+hawum9H+luuuM z?!fDYkH}>!|;dxeEyeyu3Bd^b~L${AGmwBzdq^UqQA=(8LvOcHeS`~^|m)Ug=<~2 z^}97E*7Hn_%YGlFCZZScVy)1lWs`XxwXf7O(%!-JTXPBFwbxNv>hQE%tEO&zw~9OTGyk?$(`DQ`yG@HsJ!j{xYRF0uj#DfU z6+X4NYqB1zYq{ydNhi;VTv~D@X^#R|v+$g-q_Ab;|1X@_`gPB==DpplWkEJ8Ji|0Y zs~3gtXItB68jzeL{z6N5GS@^L*c_*_}$Ul6kwNyelX4dN=qT>*y)3 zlX>#%>cq1u-Rt5L>$dW&zqecX%@@<1dJ{_S>|n?dT=%6Wr~2Y#HwNys>B{L6&oo|# zUn!`(bTwN1&dg=0%+3eq ze#fpmPRXlxwF|eaGuKV6GM@Ub;p*Iv@fof~49xyk9lSCN8}EENS~8(X#%0Z&wD30d zLk8cj&kYFdS$N~I^u?mh)A>)jTQ#@DFFjUTEP8Fn_gaq_MZdD0?S3o%wf@`iY4yar zEctm4^eX3V&EYyLH}{sg#QkL%n#$FYznkZB?y&nMp?K-G$NRMtqS-!FpE$j9^4_=) zKO$16FH~#WzP(p1a=B!m+R3k`K9g6Um&g}-J~!s`uFVXMi>;03ACKbI&B(YCo90+D zmuu&x`+Fy48gcjYH3zm%JQ@3!KdSi_hmfV|G@de9UtbCFtGy~YjB8#d`7etWHWyr_ zSz|a?dfxKWA|}^`lvn&(zHsgI6y4mU2Ybr9Vx46caX2~N(@0QBIG*e9xYo33zk6C_ z=@U)${HV*CZcL9XAIy6?Ek3A_!OFm9i|hMy%NncaPM9Mjlkarr!|5AL-xuekZ1=iR zcJ12z_Oj~DHC7VI-Y*}RiyN~BDkiNcIK$@Od3*j;g$;3v^?eda)1C#`a0^Y(;decE z#eL(Kj_ib2E^#k>i?x`1dXIan3cUHP5tan}R z(>CtLRpJvR#awS6+tqNz{AI`~H3!+grR-Yn6JJK3pDNz@W9fdr=HkSB5pT&q|NKAP zHSM0=Rv6kRDRe2VE&biX(%!D!{VQ7dg>z4Yo#<*^^mLmx!)fN;;^;>ffGk*VedSvJzmHbL{u;n5YbKP2D?k7T; z-%@{FUAJ@N!y5f3zuSH9s-MqamN}`Y``#4R4GTZ6mbe^|onrA&x0p?n?Z{ej$BdbL zhBb!{8@1*}d2P7u`TTWwr*e8Er|N_pey>{x4^^ZyZt%PK@ps<0Um<@R9`g4K3;tYk zs*309)Aws^@4mhhd$Q)VRQd8?cOe$nFCnoFqKgi2cxQnPyTtBV?W%3j)2CjI z;d^JmsXXtm#?BwLOFpRFG5zXgyTv-;YT4geZ$-a2y*k`>$2aG2zy!rh_ZObuVtX~d zyjbYu%BXvfY*spPO>jG6V4^kUw6llm?(K3LI*zxCGuQsUe%Z0$wCa|{I&2~RR>EzE z4n5s{u_0uI$@!MsAFWTfww=2cy`A~!-IcitUoD<~Tx4;#^*;mWF6Z)`Z?DZ>sRUUT zU#bkZF)-QAJIBgdGxpgeo;w-EEwi8X^B4*2T2kEjZqbg~NzsQ?<`!$76M64)DDSOZzS!YuGHeo6#kzz|Okv6L&V}(g;pZ4kw1hjWKibY*tSWeWYxxX485v*=O>! zudcgxP0MrLzQRN+;>QMmqpa26_ikmAOv*^@dHA>>bV^@~>O{c*xlPCT*&$h_+zS&X}bGp3VaQj2E?E7(N zPdvz9>hIH#Is4o9_FEphKit#binnSie1Eyhoq4tSOu4F+T_uT2rXMI&dX!!}AvVZ| z?WFiQ#pL(btP6q!zbMNo8AQze_i7cJLQ3+&%Te#{6x7KUAF?RV++kCDo`3I^O}F~f zEH{+JcNy54-P-g`#_HQIk4uN{zDws^`KZ%-+r({aG^|su`x;%4@Kcv{+r4|W#o<21 z#2qhIv3nb5U3gsJ@cj8K-IQ&eN?SYFXLejP_t|gyBjBjY#A$a!Hd}sJ)qB#WeP8GP ztvffiChgpwou??%b((o)7>Apq))gZ)wWMkbFXdR_DXViA^z`!F+-<}<)%}6q5&oFa za_-!`jb}cc{9H0QbWZl$jV$JI&RGUW>pxpZub=7h$?utUrjqkA&-2O?Y55|BMgQbdKFLL`=FL96QNX$M%jvx0Bu3%o%e_-)8C`C+B{>*pVML8+^RR1Pp&V! z$^PcNUYf}1TM8~|o*P<(159S_XV5R5I(M1&D%VqHFAh&I&9XFT3u0#8u(qgXVL|1y zzH`4DZ}tg1Su=4-@}AyGw%`paRg4j{Z|d@XIdsG-Vo%F~NrGz>?F4RoS!Xrjkm7-F zPOq~jBy_x;5_xj!6(OINw7s3867G}Rl6Tw|NnOtHxW+*;M^|l!%YkcMCBJ%WI#;cG ztEY4G!OqSHbN!;!g4XcQ<7L=-aKXXmHEL21QamQK&zz_6j?KwrNkjAkxzO0T=gSZ4 zHnU}1@|2vBXXNl<*0)>Qvv1FPcI%q=&OnJRSxZkazkXO1EGK-(;_ZF@IX6H2?B}1_ zw%p@16Z_T$Uym=%pIn&2Hd#gP%!JbiN=^vo>#AkkT|Ud@(4OR|))&i*!(x3lIV^I| zRSkajaJvMv=l%P^4{MCOKU#OT^K75>@X@Ig9~UNsz*Tz zch^+Pxm@kMc}}-J=lZm3#;1C?{7$rcDo>gC_R;Z}0D;a2ywZkK7fVH7k^T2d_-0PH z#IhqU=N@gl&e=KR?xOruo?|bZdlfx{j!&DlDZ5-RHu~_1zU8u6leS;D)%{bsFzp`e zj!V_stbddioxA!(x4KYdQ}m(FYdF_Ab4}xFuJ!G6n*QXKkmAno7Y)sLKb`e@>M%jl zaB2+mnd@f{>#92`;GH0a|laH(K%&y2$ zH&%akQ6%Bw&+PM?=SL}LhBicTJ*?K_6!YHEdpPprc8*e&9OlM1KZWDYR~Fp~sktF` zBzuA_qvc|~->bE+Z+rCZyi7}4#2wKhEBVb+<8S5}9;qmo*uC#zjLCf`#e+BdOXZ!8 zI$nNraph*==9oi@J7PRO?NGG6z&-0h$_b$rUC*Cu94k9?a!LCAb8DtIiInYZ{pv2s z^RXx`_=hRC$GaVe3pE~ZOWTv#+_BK9WO~sVr*$>TrVHPuvpkYfvkrSWZSooAEoa|c zT)k_ifwS-#y}6bPyF@LY&29EQD0$91X0@BZoAXmPq_=&q3CdtjRCeF#Dg9bYZ%ys8 zwr#sI{xc+TTv5HWq)NkP->t$UYb%Z(=M-PXGDq)_X-~|m_p5T)dv8<|m9l~NwMxtD$2#(!|P z?!g`P!aFuazf(VElksWYR+mc>49hRJsy&L`@Ic19{qEt~gNvIxS>??p-C^-$w9wXg z+26lR^V*vOjGgx+89z?l?iF)QWsdGM&zz6)JtqAuNCm$G@L&SA~WSPltg zsr~@lFGe!?4L8n3ahw&3e(B)RbKwu)x#A4<9Z4^aYqUJ!2@Eg1<6SHwRU zOJ__l-XQw)>5)0c&%PNSxp3-%pzz8tt-5WzITpVC!W$pR_;*I%_Oh6>YQs(aGM`4i zIh*v1zi$4p;PRqhyIuA!F_U?m%g(xLDyvnP!feJ`uLB>iJ*qJk(!QOUo0r-anyB?K zf>)Jmo=fk+HEXULonNp!Z=T@0#XrveXAs}D|J{xym(QAR_7t1lmX>n=$UfU+UrS>9 z-&#j|mRW?JY?hk3d)rC5#M(u6qJC4w42q?XcR4K$-*0@lNxePhKZC~UjziOG*sh-8 z37A%VOex{^kFD$X7}&24b-L6YncS}UMX0uf*QM_9tj}$W{dpE>B#8$mZrAO9yx^1L zqJ>sFM2;DGGn_G5rCyMdrXus4q4KhxIMd!))@FPWf*&OoCOrK0UW#49x~J;Vyt4Xs z|0DU2ABvseR1dbFPMJLRp;|1c~tKK_7Vacz+)Z_1B}4&U>e>`doBmWgjJYi~bn zRj@Jjc$kx#*#R9R_nueE^Va-lc)iX%J)3*Z_a+C1?Vhi%7G8~cqnqt9+4$w8O7*7y z4DHAB7q(tHAu4TMUUTQFjoUNFI@XQa3th5yo@02j`)A;`FxxYe+*Vz_@8NV*BvQp# zCc{EfedST%BfHIyFWi~E`ZZ_bwhmTt+j~@)ItxTHew4YqM6;5HaPmweU+<-@_F%+kZNLI5yAX zSi7NSYGS|rp`~u$-glhQ{ZS(n=&C_vh_vasSJ)b z3p43dnw2?CLvYE|b3!JpGv+I>X4orKJ0HGezWd?lBbzoYNm+RJc53Xdc}=s{x=izS z^DtS}@S@PQ*UGQ*!lWCqev|z3^HnxFKAPp?mg?BNdsC0T-d66J>RYG2eHXZJE}xtw zV;H;S9og5Bhgmk(%)56|$oS4j@1@&Y1BA}3UY@}A{AN{LD|=FzrU0jH$JVSKC4CjX zSEpU4^;8CI_efUxVsfvJ|CmMTj#MI=}-xd*ZNJ5|+WMo-%{&${C8&e-I@SI0g6 zlpZsDv_x#;wO!9mHl^x35(~d}NK6h2}=`7QQw>`3$vOD=a_odZh2X)1}Rh*Z2UahhW z{cyIiOS$8NobjQF@*R=hIei6vuSzCAT|F(c%8Zxw&1``!*W;YjB01HwWG08@xjIg` zaEoEnO;wj!jaf#g+dlHCnxB26yJY9=nI2O%GM{-oVRibO`*KaspH2Gk^60r{jSjaX z-;-|@Hk8TwN-j8Z=$NAAl6SYSl!na7kFw*waZk0dWOZe_l>qbFjc!5~pASmwugzDS z@>QQxO_lTJKUQ7)m8GU-& zzj4aWJ-a#5X|bW>a<7TcY+jod8++XfFY;qpC^jcz?u?VW_gZNu>DF0a*mQD|Z=`DP z7lEUNwp%tj7VZ%`(X-+~9kXEDkHG1+Gv4y%Dc6!i+0(uE#x! z@etmjpg48;wgzs~7q&GU}s6^ZYrXsu(;eR%$wXwTz0 zQFeMCPHZ!X=xCJ6y`s0)CGQ5aNuCS8sM>Me7r859stoii@3c+Kn{-p}-aVJCQ(ktJ zxe0ERycyFY=e9~%`E4}c)ri>rd=F&UPO;C*SiAdYQm?1g46P@RCtO{%@%taG{|s%X zHr~5^GGCe3OD0+Q!q;^n3nhIQ@7nJB@UW*bbCcX7{WW`Dr0lM^?YS(hAU4jj?UwjF zhoxP|mUaZ5c~}+j-G;wQTK!RWrurG-TRSed+-W`MZ+n)(;EbcjjfOL=7I|Mku{=n5 zyx{Z7Evp{&zF98$=#JF!j=PaJxMxRBOq%ud;!GuFh1BV}U*GZDuB=_!`t(e&Z$8VU zqj}7JFP2UfFP^SE;lr%MFB@2dJZB1Qx$bGqz|m~CL~ReBGgI7|eK&iY_Wg`Ve;4dn z*x@y0hQO_3(z0A{ecmV<7MMArVt2C+x3oijKoy)w9Y;@m$k za-}}DZreS_ePzm|pa@RGhjOy9#v!lnAKkH5UEr}z-0e+ef5htK6gSK~voTI2)rdFt zS;&0OJD+9E*8DLmpKj=_yN6#VKyT+=kxh&K2~Rwc>+_(4$*1#_nBkepOi#GaPOa`w zc~bRhg;+qPh<&M1|K`|*lg>ZCSn#@S>*e)(I0{1YSMa4&T{^5ZuXK${8pqU5wuGiV z7IyCMOoha2o&?^pZZ6c*iYRz(spnOlD>X^{-ksK3bC$;jrl(G3TGd=&E?s4IBysPg z=MoH$vo5e(XsaI9&){$*D+zT1i3zdp4>*5T{=(C;Ti zAMIHe_~^9Eti8q?U)Zdu_!NESXQ1~~2L6ms_N$#s<}KZJVArlQ?R&R-N;F(jdJ(U5 zXyxP0Gb`>3);y8!bokD$P^YEeddyyRO)_)Yj&AjpR!6lT8JFMj`2=8d0LCEXXv=$ye*87}vt?1@^Z$2+%~z5yAtuUsxTe5%#d zb$iAuhtkftU_0Ie73+oTeqGWv`>^49vEoGu z@N?nI4<>1mcQdmVMPDuH+IMfy`iz&UZ>ttp^?Fub4;S3n&#;np#jzTtH8LyH(*loq z%;@^Oere%+FOi*W`>yyKM;@+>*59J(@$-AY->hS73r}rfVQuo%@-hrMr+xI`KfQg4 zaoW2gojkJw=4d7dOf*`y$Du^(3XitZ8`QhMB+HINxmH!g(38ZF4&hSNBS*Zg!VCGyB+IcjxHRwG%80 zwR!h`R<>IzF?Cwf;S#p~?a6C4Jaty$xae0o_r;GZf@!hw@i z)330GtVupTRaAO)@6JnZQ7LN08y)T{GNk00FI?Ac5&r8y(Q+X-nLMM+lvB)dGoHIH z`Ps*jpH~yqI&L&I&vLFkG+2SGC**#$>LY@kGtJEF4-9r{P3mkl>!C{udPca z3A`;3onmmx+)D9_#C1=W%2KAL-PprP`R%l-nOkpK^3t8*1||&;igtZdXL96x z;<6^$@zj;Pz^T=5=lY$|I<1i`Ahi6lh=a?!Q?dsol&0wMmaeZ})-dhhory=*i>$e2 zy6By)_LUuOnX>2R#;3~MvGWku_%+#HCPYkI;_8ybd%nHXmu37}%=_-;)(Hg`g3F^~ zS^dl{7Vh}qd2_-0$qy>7 zSAX`?EIQI*cu!XS@R{CyZBuU-+@Eaydxq|V{tlb#LOK}>+s-`EIQN_P_5CS4H$LQ@ z|4_KSF!!F1&E2MT^+mj%b-ruW=5H%YmUk7HlxcgSMgM(j=0S_wK5UoU*pP2Z~JUjII^=IjnV_+x(dj(YCLvh$A~ zc4u-f&Qb}fxa*nxc2i$U|E68n+4n6J@^jKjj)>9K%~oSLbN0x`@aPZ!E=HI6ZxJY4 z;Pov@b=rkHr#9`FAmkCo_k2dwnQwY0^L~dP7BZYM$?s>st>*2mj(neF*EetL{hCMu(DN?kC->HZrenUXXPzc9a^5(n zUVHS1env&T*u3b|sarC+?-)*4EL!LDWZuj#M~j1{JzC~v?_i&&y|D14;+y)^We*C^ zY?mqTO3Z)Kf3r`)aIxKDh3b=^)@Ute+H9a4-ZJ^r-oPWK4KoX7Ui&>=;zGZ5$UeWkPUxLSzo&)zN}F9CMOy>yX_+_ztv%3YDm?XGvujo{rMJYm86 zANBI~>X#4R3Q4ZdFIjf-S?MIFCH(8w+_h(CG zZ1&>J+2M0?yLDInr)Y-RTiTulcf3iM7N)*X!S%wfa@C_C$0-t%?S-j|6T<9ur2I`vB9{?%Sv*g1dN?A^Ru#(B## zZ9d_QThH{B?5o5Jx|DS{*Y0!LbZ!6S#Q&HFjkZq<&*pYIC@yOX4yl9#0Nt@xFv?#Cq!O$Qz) zp9z;|J*TXcTDiG}dC`l$y5vv(`FCaHv@61rEFVVs?mt%;sSz_fY}*NgojjKWcWro~ zoqBW1_Q!W8)Sb3@%`UWk%AJCbiidWnY-Ikfc7DI9>->}VMf&o7_nE{Pw;U+F<8x`D z-r^4>lbdSmqc#Tit&nW+PKbAKE82G4bjC-ClG#~~clJJx-27AfVS(Y1?cu2xt6p3+ zPQQEl?fQMbPsD1>`#(0B%(jkur}jCW=f~fZVl8i{OKyL>$z66*d5w1a^5T>^+ZdRB zyqv>R#g%$fPTE}fr~P9a{kBbV+PbH#-6AwKtf$4EJ8UH_;%2*kCGXB<3WW+fmg}-r zukjmIFol(G{HFAN>l{5^tI7L>jec@C&i>IGdE5H@+;59J&P(T|Wo%7W;IQk+{CjL` zZP%OoPxys)K9Y%gWPj2+I#Ts#PmW5)*a3pJ+{;?~~W-ADXzK zUY7T)?y_q#cRX#UMP)Zu&oN@MUcI2OPhUN)TKZt_+Xg>P-QC_YJ6IRLciVn(p3kk1 zt52Jr`_X#!KZ9tsr{2;@@7}EMe46yWXRh|7D&eU8Q#D)WF7|fZx_9cH!iQD7PZa)0 zgvD8Vuh7l?F{ejqyY+2`duAF%oN|$(ug3U0I@~Tx7OOkan!aPsweUH%dAf@=D>c== zInO=y<;&UZAKy9ar|5|u-tnAmlG&qgA3E~a%spgwWIkWLU9{wl=GdE_wdx)1e(u(3 zJGU@g-*R@b=~bvoA-9X!&WwIZWdf;o0iPi zS*IP5c;WDYPkKA7x4*Oeaq0U{i^qq%eeabAeAt`w?C%m^C8grkex>Ph zGrlhT9eG^GV(Qc7Tn|&`i606$%(GWzGJ|pY`pp|(?|kjNLW}LHsR}zsw%^YS1#ZhC zVttHMBvtZyt1jMcY?BjbmwOz{I^pK68DHie&OS5CPL*N3wfL<#`=h(#GgNjbW;}AO zR{3yykxEX8PI6)oPw8qwfBv6lJ14fytXO($(c~+d?D8qTp?_vO=&rL^E?K}8dCOSO zUFo0A--)yCnhTyV@{2e>`H9Zn1zqQR3;2~!I+RV`Iw!Q^%Cs1<{28~F-fro-QeJg; zPTMr6eK*VQ*RGg9H7)!t4~Mj{Yuen8%(s(1$<$6+zBx?4{?+n1GE{x0h}-}Fto?Za))qlfOs+H8u^&{`K9wpaN>sQ~*7kr>wX)e9PqB_`g_QE0JA z*w{Ps$QwV|&8!-4>UqUuS3SHii97P_dbz@5ZK3&hTBUE*UH_=LW69&TyQXt{#D!c= zW=!7L)9|QTbEtWxsPp?w@sK3$LNHP~@&Z&VjP+ zJq7>Y?_7I4TYtiwr5ijZJpT}qEx&WJZ^f0@hpJf* z=tw+@{$9Q4UNY+ed(N65SFtFzP8 z->&)5$DW0ducCju&24_41@xXV5{-M;q^Q?~^VHv-YX>c|ZN*#D~YkXFQLx+~;4>H9_}O%&`a~zcs5j-1K{X z`n2iytVLV4y!qOmc6iNqu}|fn1&Rdsr2Ex=l3IMqnt5lM7#oAs=cKxs(ue%H3%5_I zC{xzUYwl;wPfvWfZI4~vr919Ir*-_yo8 z#>iVZpZk#G6_&UYADVkto^74^cy6bS)dB7Mk#}bL?o3$vU43@$j{IFK->9diKG>7y zea_gV;NDT5$*za5Uh@<+bD!qavZ}|9?e|5E%VkoIEp5&*{+EN#?XTop++b?xSTe1l z+;v*~_Hd`)Tj%(_Fms*FU2@!6;8K0n*-VT3GLNUt?RdZ~dS~U4^p0>d)%C!a-1kHQ%T*GmmT_P-nEPO3;v0VUTFDn+Qp@}k?YpBW9!`y;m(FdNr>B0PVt(4vsfl}LteN(! zkNKsRSMx9b4Hx1MRo?Pg>m9r4wsYp%(Dnf?pFBhs zd=&Rp^|Wr(bFy3F3O*K zZxYkW%Zra3Ph1@-xiDvk^Q>AembpCgx_LWP*YykAZkG!>bmr2XyLQVy?rJ*PACqN0 zt1w0EaQn$4d7MJZ_PTXYYKLpsUWm z-FD8KiH|<8`dJ<+zI#%E_l~0V3+YdP17}T7{Bc}p_oMp;`*uzG#y#VS@NsKN@zis- zHuvjf;0VJxt8`w)Jl`+b;gc&*jhTozAko zlLX#0yo){b_DtQf^0hq`FLy{Rx^Ug&#O=8@;g&PxIA*O=-MF6b$m-{c@1Jkk?s`OM zPo{GIT%8c9i{DOfDgIEIyTf&M$I`yL)~{P{`4n-i{p#Ls9p=0#-fZt%*4}Ng&y1OF zZ4cx8(tmp0)b-VsMzP=eJA6cEs5CJuoNzH@l-5*8p7GjYsW+=t>!ve#ywVRmHNMKo z1~wetV6x-gS z+TrDXbKVK}qYqsF%zV_cbk7+^L)U!|%SFFq zAri5XjGOx+j_ba9v0(aE+unzshrCYL9y-|e?82ti$x_Z09nvpZ!naM(Ps&+WF<&sn zCa++NF3)Vu1&Rt45Ck8gPyKDq08xvFJ$_Tf!pPKVnZ3vUS@ zPA*ub&my?;!J?!5|Ud(QIAg-2yV`gIZYyxrbMQy$;(IwpNH=&-KxccKd zm1ol81=73v3nKV0P5u_L&+o_kjEeqD*S&Y2Y!B<}*ZFwvn5&ezo5O8Q{^e86Y)pSU z-}om`k>hRijp4ZJ<_$r7CUagX)W-botv;v|-B9q(vbxwtxidOS@Uw+Mb#G!%l6&k{ zYjuUP9~W=EnzNXJeKIfK(S=*)ZEAXOhEJs@Wnz2Go1OFJ_f7iV8-MTpqUugr&ObuO z{qIKq(>?aS+Vm#h`47L<1n%BFkUlMj>0|qmcE3t7qlxpq4t%)BTYf}uVRuPnipSUO zHoI9*t&c0cygT}4+T%S*oH++}Z{66LtG2Dp#jW$o^uynK_FQdOp7lUw)8g5x*JSRz z+vRTAcun&2rcL}C_eA~f{;eh@ETRQt@sy*^K^m)!1Qw~F02{E(Ih?*4sG-So)Cdnc-|mPH06Fg$qJ-kw*w=dC`+{`f2M)rPbA>EAObdEWoR;F*Jz>?p zls6h7NvxuYW!vsJU3=y@r7hw~;xoZ!`J>*`yeAe$=2-_HnZI$5-Sy2iqPx3qCP)65 z`=;9C+-=tIXO{8>$JfT*oOhux{f&J_ens!$nC=pTD1S|Ttv#m~PT!NWQEDb{_3gf6 zAM5OoP4D?oD0`iMcboD3?<-c#3itEdWMd(D+r&OYcddo|$-Vpex9p1ETwCFD`RKg2 zw~X#S>-ij-F}3*mT=R2$g(nwC+&!qT&|s``zv}P@Ihl(2Qkym&3(K6C^2Op7H0J4nnlT)&8`n8-uAHGai4n*gSO$P@XY()mZ=|e;rO(D2QTyYWg_c) z;!hsk@$~SK(%Gxq-nq})eu#hH22P%B%TKsvJC@94`CjvM{oAu&HkJ8rz7)H?veIhH z-pAFhChMNxlgzL?;hcS_LiRsHj^3llW!v)}w;lW4_vJzM?}*yA6+c6F_37!S9(rW& znwZhSJ~8J%gJ9_53fbA;CO>)l!<{cRPd&Bx-t3}--YZobKj!Wz?)fUZ__Eam-D~2l zE4Mp5FW!@>p?A7$@s>N@Tgx&{zf5*7)tlX2ZrO6AD1}KsrNZ$TEAyx3BfRIc^-s-R zoXGLz-4r+XcMjL)ZE9J9)3ilWC%N`B%0+Ueh9rJ4zyC1i(zloI z_0xAxzCF)v;@)@Ob!&Ma-e4$Or^e5^GtVeeEF-5+`)zgTH^*i3EP`)OY|{9e+~_!Y ztIZqTg?rzYbGJ)|&hWVx(IUD1@3A(?j$>cnROIi>stZaFeQXwEX8a}0Y{iUEI{qI8 zw)D3}*L?4cib~FW)@NiZ*RannH7rAC99h0h@kn&tW8Z)Nx%*0QRhKDi z=k*)ch&%j!*_u${TV#@g=KdzqH_+!?)hds&Xqkpj9 zkJ6u#8ECO|K3~sY(cV=GaxTw*bVOaAPG9#D z-v4uH*7?bk(k6F*cu+rUxB8)~B!>v)!HCZ;Xuo0-r4s3&yq#2tRDH|7~0lT(>- z*2tMBh#|D=CA0DhHmAau9@Vua`PHFERDvgpG_fA{d+Cx=_hi|uZ!Q1aYvvY6GjExG z$@KEisUIzVFO9OWlMH-(q>25dBG2|iD-C`u;{26jicJ9PAIu=!{POI<)d8#THi%l%JonCJ|%ebR(p0fVQsF*^|>f5V- zUc0HDe^yFkNxIbS*~zJyUz$t9+~P zd-An@9Dk*KsQ;-$%c<{s@=8BU_!VNZ!TS1*FYBE?%`8)%>u##*Q2G9a_Tkps`N!j0 zU&iY_Keudk^G>EcVV{1tX&vQ%vA=I8_oa@0ug{Y5m4CC8j2uq?cAu~?Nvl2Zq+My& zzQwEGt+6-|l`*MjOR~VuM^#<3O)uv6Z5L*9`Fpg~{Ls{+Ip^nmT5h(jv-sfq#{s@) zcdK4d{?%sJdHk@#g#a@Hwr8z94aqjs<5aH)wq5+nz-qC*!ObR??JIY>tE9Dj)Hm-N z&Z4@PrWMZ*e7M?iZN}8;k9ud{1&UoN-xqrH@w zoXW)m3N;h<9sI<;lzHyQYu!!9+z;k9uD4xV@H}I;@B8efGiPgTJy1O7@77Nfd@D^f zCAG}$N*_6zaj!VPG{wYqT4%|9lMo%p6YDD`zuF}IH!;99#Zp;vl|{2%Ys`=0S)bi4 zboPAa;@Y=f`<7K`bzE~?_`)KG_4>++Ng<(EC6o?@uWu;y-qG}^OHF)k_6044D_7Ds zg|%Kk(tmAL)Vjp374KuYk5(+NnrQlIZH?RQ>s}vD)<`7niCeVDPs{S&9;S!dhtFK* zlN95e@-rboLZ{G|nM3!`SEXG=tyWzLu!x_fo*QA54b?-_`mr%L5;gsovFSnLgGE8)ve5&DkLWxY5 zW}c$5%Uto0HSwpDg9Rq7k=*{$#i;j~R&UmVHp^v&!jm}DM18NklI7lb>Ot~+t!0m% zSx@8Te0R6ZVtqDm*@J%qRM4&eEL%J&WT@Ue0i06+a%1f%*lPFM&5*btcKfXKHFZN z6tjNInIixFCW1TXU3;>3_ZIiXnp$p^@3dmn{PV>BHSzBK`t$;$oTk}S|CZ|vhXW5+ zehQzHHrwyXe};{^EOR$J{1oRi(NS9R$+R$z1;^IBoR(y_L-f1oqf2+oCU<_6t~^ud zZPS?Irfj%4(=XSifr$+!7|O#4)RjzbLHF{~RN ztIRD9)!p*GD7in=^VIvU>3y19yPo-NxhAl|M_=XGI_JlZasFSM=Bh_o`Ub@`Nitj5{y9X34^vOf3iKHoJtkEWUJ-n-j4)~SbEYu%dcfFk3Cme=em*FC$WI`x*}CblGp?`iFQ zu{z#)d^c7n8dzUj4F>X_XKNon(S z4?B`~_}o6j3g^EuP3}v}j&9tW_G$f8hq+4*3ZD3(FfZZi8}8f3cAhEpY$%?**izfM zV_Axk;FqkTS^EXn=Q>{dw3vbG)g*aUnUAlXl`h+gZk!jnzV)=~-dnG=THB_|uXbE} z=$=_A*;f&*Qmk-+ zWlx7?$3u;0(#!U)oU_rpP%1{;s62VPf|hr&$XaHVR3V$y4?f#!R4m_F+B+-w?i()N z#xHiiG}bQO)}ioDw&38w+cqCE)LeTXC7H-RaY%a{`)lQ$@|-5$C@~{W=DeC0)g3B- zE=oq|Jz04!Y>m%xo{e|1QefZg2ixEYHcl`{jLf(?6U~tqhjh$@2L0x!F7nyE|lp)>RrV5k6>{Sm3dB`^=@C>{S<5sxZ#|_H^UI zOC>XT_p~4V%bZslx@E2Knh8RUQ&!77cs}3iXUU$^22z0=r2XC2Bw7j^=#)waI@c6m#{>7spN42*r1i9KD(i>zm7w&?ht>OXV*+R^mM!N*M! z61Tkl6&$(5kjbb?ePz|cLyqShTTOb`Nrv9%EAp%Qx>#*@k)o&3#UqPs-~7PUn; zD|;&~3HE#LQ+0mJnd6Di!&UqFwlziOJ=IldSR%OG_NU?ZfMT`|7Cx_q$Eqe+21o~3 zot<&&*CH{&;`tM{U0a=aqe4$#eSdpL;m?VSHg(BLhL)TG9`-Q9CRkW9D zGYhKwInN7`I`K_l$+};s6)&CM!SbWz_v;f+j!r&U_;{{C=-%I5F_$N+y=9Z;IJAW0 z#F447Z=YN{v0Woh(oeKxlHt6{qnz0$*HxdUsc!wTsoYY!=jxoGw~-4wYodaKpMN;- z-&%RVr6_)O;3gxFTjw8USS*`ma6zx5v3sM==dKmPhqqScE*I#YYjvGvq09%XP~o&U z9^BK?9e%~n+HtdCnT?V4jJa!@<}>uNT^XOXKUF-S59oUFvWoGfnqfTEo_^N79m0 zw{!F>T)zBzg}u!6Y1bL1yf+H-5wgrF{1eN0z+%tr&JNei9%ZFDH=@5)#O%E;GjX24 zn%DUPr!6HI(v|)9HN0gPpS{I=x_IrSqoOPNclXqKI7S>|ei5xUy-<3w?v38$OWjk# zK5MYKiW|G~OZ}Y1<7~R6Tj^hwi{AdKuG=!tHkeoFzsdXB-Zou$_sqM_F#;B)yOVXE zC4b8ei8`|Pu*#}P=4@A4PZic>zKmucl=XOgm3GaWscsU_|IlO8VoxqY}au+4VvBAZ-6 z+kmOoUrwyjd#o@ic2>D%rF8M)E1yp;%jJ%_$8^|BJ0X4DpB2?#BEPCw9p2{EA|JuK z!A`U*e|PlM^*aT+g;%*2eOxzL=;dVA!a2pSG|tJV%WRx7&oj}p@%?Pa(i=H%iZ^~; zdYx05lc#r`>qk$)ZyT@tJ{}k#<9Eq2aoSlaPI1cw4~?=*N19w3s(N4bv5PSrzCA0z za&5`}kd%ub7IjVXR=v%<%u;su1Y4t1mGiAuKijriWa-;=hH^|9Go4nd%O)$X$liTy z|IAytY{6S@F|aw=FIeSn{PJmF-;dkwGKH(PkDc-FnXx3C`4`)5zF3XMeukJy#lp`k zIa1%KJz&t)GWKQqq2xKq=VAA*iTyfSU2D=!owL<-Jk@LFzIu`)I7`RD;i$^$hqE#y zY>c+}pAHgQF89&p&#TBysmaRQ&5|e8KDpp?J9L{5^Q4+>3-@kPFVdE+xHltO;{?M? zo1It73tDrMi$hOq1}&|U+PwGu3C1la=e9CzX-`%=u}$~zDy?h!v)xj(e2!Ga2?;Yo}NO|MX!&WbIz;@nfSWvV&umsvskx0 zx|OFGqm%boXGNKXUDDK#`vk5Dmv4~!-F|Y9@Vdt=Mfp*Ay&mOi-`aK`nRu~0C~mb% zGP}U_3UAMIp0BU1X{%hA_eoO9Tsq4s~?#co8dL5`e^8;Pu8Kg0-`t>f5j#w zpY<@Elw>h=EoWaxyr)Tzr;fMzmLrq=YR)>kI){4leZA4pZm}+;U#laDFXX|`u8oaH zgD1yvt4d}Slv}Fx35h-_oUauuGs~~o{m|2+X$q&5y>)g84r zEkB@RukvF~;r=d(##s;M{_8bY_}B5)Z{4!Ww9F^pZr5&OUdFy_c3kYO4-!is-qE{f zu{PGkBergyPw&kq&m%h{)0Fv_%UT5_?06)ZseQKWw_^U&J~yex^SW^*I~Hqa^=_z2 zPIf=NT-{M<{#IA6&-OYz|H|1vhTW3gnJrwt%4L$`N1nbVp)D#8GACq7v*xbatTng) zW~de09T~w>+gRTz7R)^sayP9zul>YZpWXJ!nMW-ew(Q>5-@aN|;@K2gHUAYYZ`k~U zy)FgZcUL?AJZ{s(pAWxZJK}qMd8CnJ^X;8^YxnuSTl+5Pb#GkM?SdI6D)YqR&kE$Z zRP$$FoAmAW7R}=pZ>ER6{>>`1Mr=Wk+*{*E$9;V~*F3w+f8b4)%68|ji)M!|G8D>#4QySP_Ij#@YTd7W63Ao?_B0q z4x!9pRz;$7glApT@UgZ#{K@z2&p(guJ+fq9UUL4&wdw5dX7d=hTD^H9=$>C$@p@(7 z?2_aYl1At13u9_B#idHbRn+wry-$=D`{W7jde>%Wuy5Xn$6VzLb|2I6mU%umOkVWW zyW2IH(}H=tci!l5^0S-p`^D^NMTKj3TElHz&Me?7xu){&;pJ;Hk0x^SW*kXTy4~3) znanzO>mEB3qZp6ZE9dMMV6^jEz{M23)o{@flewppr~TEiKABP$n!jRK_#ICB)uAdg zVr0)x<=pb<*JM@sDNF}aM7!4Ro~rA1Aev!{mTmTl^wxmvW0hBy?clMoS+T<9kl3@F z(;GhYmFV8?RF-(gmlb3eF)<+Z3dee$YqA9ap>HET8hu&y{iz$Pj*GtIYDL!BcS9e) zUz8{nZhBmT`>ar)w;G$)_N>O_Ss!MxD4kiU`6G1I4O6GjD}S#(qR+H8?ZoqtE5{1= zI(%Nwu*+{%LSxr#5zX^QpN2NZ33h$aJJre1)#`hUHN!fwtgDz=&GV%T0(EMiA z8n>9ASHs^qXS}csZn`L{VTwCpuzLiEr zzji=VM7qAgg2Q&37F<+39ra`HR*Mx@$97Ki@5sp#^jf;{d9=+#g_KQv!ostR?gf09 zYRfCh$g|7eD^za9jOV3`7t{#9_P&}{xc0Kh{DrTA-*ugeC|W(YquZ|ZgWu=5aS2ie z8GHp>7H{2VxGGS_b@|Dg0ngU#DxG+|tjOIkXje+JhUNY!wtI;(XO?80J8)T-^|0K- zglUDm7Ui>-iA^?}v}g9IwH1wb+hfl%ly*-oFxdIAdd0&+vBgVXIdA-a;oc>lUYA7% zO$smfR%hB;;p-|@Os9^_ez0d4 zW#2d2EqL%NX!g`2E!z)uZ$7wl?}Trim#&AZ^xjL5sSI>IvxBF0;$EIr{X3-lwrWaV zHPu%<*Yki=N`!3&TVD6})G$jE-BV3xwZ7OXy4*Z|kKsu1`m|=nt{&0nuOs+)E29*4 zc2+5PvK)AMKWpDVrL*@wt4sdY;yID5@a5C`s3(7Jdhi})&fi9tzcuMy9xbx&p->(+C# z3}0KY>8{(a;;$ZyEvA>+&dHlUt0(2^9P<`!$ImmPo!5WiU2U^i+F!Uuz&p7!_Q0bb zY(+*hTuvLDC}?!!vkZ-KGFGvDAzAT_p-3V8?%S@WjoZ7e`=dVZ+fl0MVDTqp1|J_E zlfsYHYMrbb=LaqMF#GoD)E%jUJt@prf^M!^(rcRI|Ddnr_Ve#;wmR;`Z{A*NdSrN* z!Be3&^>{~R;7hAzI(iCBl?xZ2c3g4nNav|f(-|aWwoR95t!Q^EEfOs+dt`KmO(geu z*NWq7xmVs^v2011g5%pboztroD%`D%IAc{K-)^_gy`IIQw5e(FJzn8VZC8!NO+Ulp zSR~$wu1b4Uz4Fex?FKbZc=^gIeldF|Nb$7pT6d`1TrH+mj`?f*T*t>fZa@B9U-+^s zbkBvTTy@=(lPeV23zQ?S@HE|i)b6MtbzY%dwtSg?vi5$R71GtOZr4dYGF-tkKV!a? zy3VofmQ$2H&y^feHQVL=D0Z>>QsW(6_bipO%kEUG6d%(vH`}n7!OiAl@XuKXl~$f) zXIKBK-B(e_$I>mA*IK$pLBPQ#yzS}}*@q`=K09t|oxy&^t-#lwK~$=CPJlUw63>&I z+rm%yqeQKidDuFZemUad74-jsT)=ZWI>+DQ>RG+0WDJV9X zhTYjxv|23f*ZL!ED#AvrTtwgnBKcH9yN{yyUEOkQ4Zw{j^>F=%v>yAG0z2XE^Ip z5#(;>>@z{-#Kp-CZVTM!&t+TDQ7Nc=MWe@ZTbiI?q0+gech2+P9976EvJIPZC?-~v znbDKoTK3%4lSM`_+1;zJ zhAs&>VMfmn_Y34sqHyrE; zVz|NeyIt|jCLg<=rv6`3iXJwu%j_}R{!+ujPw6`6>|Z4+4x3L(TBP2s>C@OEXu>0G zb(kqyPDU=Ny=T>KHO>iU0)5@5t8!JR#6CJKJ>l{Muih;`Yqy?S&(OJT@6lC06G|^K z=4(5b&5C$wsV?`5&r-^I-#6Xak(Xcb8`OR&y}EMyqh(k7K2~^ht+;)Z_YcS07X7v7 zmu4>A`Mh)Pg-wp{EZsYgi*>wPd;Y?S(hV{fKR=3Z6;YYou}Dno@_~iX%&Z?z?)@Tr zca_coommU#-dJYE^LDF$OS|`in+JQIw$C%t?qiJ(ym`$JU>r! zI`W|Q*o@l>JGO-?tGfMX;9B?UQsmO?2|L1nd|sTZ{LX7}4#R@b6_#elJ@VX=`IIe1 zq;H%w=@8xW@|oYCSK@b!pLr}erJDA9`Bbaa&TzYuZSTaV2kUnvJhyvwR_mOecw5hp zbyt?%QM(j0+q5rw!p=)8cNV`qQM<+Ztdhwj#^uNOk9Fj|yLL;3-FSETq$&o#i6J}R zuf02?>1M$FTswuCA8n_n0yKXMY zv0KbX-f)&<()I=J={<*w6%5&4oZh)??w#qA`kv_;9-a{Wp;tAw=c%DbW64`3@4S87 z(tD3JRUZjn==R`AMdOcvg6K1=Gq_~D?cAy~?$mH7Tr@ZPd~?B+eUl_)`j(rxWLt0H z5Sa>?0w8a4h=C0FbYh2hl6qjy$ z%&j&-c$`dJ%MT;zr$vC%=4In7gBA(bHtfDNlO8>)bfUW*HWBC{pRU z(bj|Z(GewvmlrnZNLxP%lK%ZpO7XpDjHld>t5+(y^rBz4&Wo6>b&_*x`0V@>$rrlL zd*&9Fr|7L)8+TiOiO&K>>y(Y2TyI~y-ds89QT)_Rk6HsS=+3>gedogkl7g{$H&1K! zbBY;Xx3m=H-W_7L(uQYOschyFwyRgP`4TEa7aeY&eD`>jUWdx6x-_H8C-X}qF5J0& zH|w8 zi#N&hIZCn!ZwqC1ts&n_)Jq@`c?#=G^sa>WO~Ww@p;D;^8??n$lZ#(6)reamc3G~ zcaN>#EQXau*E9MuJNWW;Y4Nx!M8>4--(=J3Bq=zB#aCs%@K?L3S$CqllfP8Xn4_qk zQ@Z-0;3=1lw>TdAFdgiS`T0Il?+~9taa49nk4kEZ)q%@?f{bciv)k_+S##9PC+irW z@pX|9@v_1tU6-~LGq*S`oVe#th{`6f>nxId{a62J` zLyh|`F$7o!W|~+wuG{mf_(6NXv>iK_DP3H&?5UE_+YEh1zbr#)$p((+toSEF5Xj3zjK5AnD9@oCLOCG$nWD!!sCaWpi%$T(G#;+n=%eXz9%LOJ>*hx~ATr8}Q`$To*I#34L6tJX7M? zyTl%){d@Q&>cds@Vupe*7d>tMX4co+6e=Bg zn5;Zq=W*6oU9|~)KOR=OE!U~en(A7FZUdu9cZ)d9T)O>XCOx2om=uqe< zJ7M1-Gi6nW^9$Ddc>hp-bF{Q7Ii#<;TshEb?aTd-R=5_MDD<_jwaJ+9c3I<&&B4@!2 z#zyX%wIsgb$j|V-*8?TwK8Ji?XFJVez4hTM&-U!<2w|SB^?Unj5jQi26Zg`ma>)D) zU9H_<@-`-pQF!UOBduRV94|^`%FA0Zrqn!NKi9{1qso(OGUZC9n`f2g7hEWiQ&w6R zncCQH&yn}h_{VBXowM3vxi>D`{h4?E-rR`Dyo4UT!WFAdJ5V#(E4B9DbTcG?9v zuY3Ja(rK#kL$hq_SCc=RzI^U=L99UYOOZS40+$0h#b4K5S7Ep~ds@a8adDYhYxeh@ z4~Uo|7MD`8VD9(TZbCYfcP_a3eeK2_QPRfT>q=&x3ei&(VshW=VCAvr__2PyEUSYz zdb$*}Sy)teNv3f<_xh~XIrq9%cmk{EM2-^@Uqy`BUY|avPPV|62?94aLyv(!=a-0x?De8k>sCRxGqZfWDwg|anppOVZUeH zhzPX4lG6Hd>ixA<&)J#QwmEnNU)ybT9kS$l zM9QhpM>yjDUpc(F)v5n&@77SSUmjtW!Ke1^m}R*%#4rEMRj)|vvs;bccFmB^zA;yn z>GiEw2bCQ)IBxQuw^^0+F(~cYKD8SYPb9VW%ydwn@gnQ3;Px%Me_r>FUi-H6!1K&~ zPRoiPtIIKe3zcO2t8qcdLz3~Mx}x`Gox<0y%PS?FWF1*#p1ruq$>?14BI~Oas1{V7qaVAfV5w^$c@CuY|F1o#LQdf zm=#=~ySCoOGC<()BNqLR;^m)}rrv+-t;Q`a7FUvG>X|1ZwdI(O_pynAS9W}u+Z!&! z&GUZAGqaDn+`7kBI^;zzUdpj@rN8frC1MS>8j4Mvb5FfUIdJLUd9R)a!b{oy9Oafd zuEqJ}AHTd=uw+bu6DGS^Aje&AnIoq9+tGUwfs_*finf#}zm4tSg!J zJuKc|y3j>n*Qpskewp9I9Q(Fx`gfNnb>pKbEpgS#tG#k7PCm1`AmJ@LZ^gOxt?9~X zJSBald6O<$&25cT^2{z@?YBaDu1Qa{ub`sC?WyKLOB|UTuc$`*d9oaoQq-C5w36LI zH0QHU)iPzp>4~p{yRNL%sh#H#qOTj}(VTws4!=!+p~;n>rHk{Oa_dTJEcc0eM z51W~4R6;LIF1F+{dHMX^s#J$bC)yumUVG+vPH0J#+t<zX#K_{ ziIsA~4aHBgZ=Z;|`nCVe?5Qcc=S8pF5qtOy-`%7I9cv}87`Q8kIm=93U(p=Idp2y7 zcJb3+TR-eblytqd!?~x=YF*Raa{at7FJf69$ZU{VIdzNw(WM@@;$9>lVPC4J9$!MXYZK@vLRWwd-)EQcuFX( zYTml-AmapqzaF2LPPa*V_bk1luFo#{N|593P$$l2p#sa!m5( zoB4U6^`_+5=qAgHm+zHNTN5pHlu>1}KZi1-Ktl3bbAOQ-*LO{StRy$ByfN)S&U&wo z4{Uk`%coY0u05RNHs|eLL4oajiZjFn^Z1=Ijx;9LerRTo&YP;OC#|zWb+V>((br{} ziH9Eh>^3&P>-^`(%L9_k%ct^P@;YLi!|xS)XYt9&Ed}m*Y&{$0G>%PDn3Wi-p=Bv^ z?!o7UGv_KFcPd+D{nL2K8||ic)57O<*t5NMCQGaQlKV@PjTRJ~Wi>CC*%j+pt9rm>;rrA{>3rqviGM<^_xxP) z_w5m*)MLtF>dd*PE_uJ`xTuxT>msy&wNJpI9M@0#TDev#c$~IYyEge%K=g{^$JXp* zikN3_`2AA0B**lfR|;w;WX`q>x%1A1r@zT`Y~_U^i; z#d)oiU!jaLdsgzkd2RBG@%GnDFQf01iq6XZxv*$~kaKOKBm27#eLFr1WZbyxa-+WS z>%#nnX$2R*S}M(5R(YXFQSWD`l*XPVF)dFj)6aG7%ib$+aHS9TqhoekLo>7VEcSRD zVQlh05O^oZAbwZx=H=55?f7mIl5m|*`Cw_Ug-peva4nBZDlIa}Gk+g7-ZC|$+~-D2 zVzhNbX3g{>x4DP;IVIQYzCANDq2x)5{lrhE%eM<#USrmX%xsT(9$H=~bk@pJrRPog zkBf7xUftq2byQ3>vA1%S?%P?ZweI>7zq95&eK4n;MZ4OxJ+VSZPo-mf(CySE7Tnry z3A^*>RP&WGe!n7VeNs=|$*n=6`Fr>t#~D_0vadBeeieP}eLs3d#wjniw#027PdL^! zB(A%s@bh%Lk$H@=$|O&nSq9GHjGJGF+wRnKsLR23NDU2JAX^ul)6& zt*H_}A-d`zzqE_he5-F)3QX5A<+7HwTTH0Bl({x-;+f;N51;u*F+DiF#$^92g%8h{ z=B!r=Vhp^rdHT1bGQ0WtIMp_9ek-xY;f{EAP?b;&&gc!uI9ZkySnGVysK|w zd|CxHu3p(bHD2_5<^I4QFTAZ8mDZQsn>6{-qKgtwGp@W+czNRULa`u~#-tAU07rAq z9aG~3__sdEzT$8((eH0UH*>-mm_RPecN)U3(F`AbHQkN&6QEudEl%&D$}byTout-zL84pIE}IzD~{G zdZ>mYWZ_qlW9LJ^O1BK5n|Oz`y8s*YVp0 zAHI9%rCi{$KB@R*#ezxq4_@0hzr)jJLI>MK!HX=(uNK^_vnyS*`$5J>uL;}QCNkN~ zxbI=LU!sz6eY6MwR~B^osAo z+ppgdI65VD%jetE?PCHb#+ozj`D^dBtZTP+-(LM9 zrs-ae=MDK)SXG&U*_Il>8A7d-m4|^4crb*j)|62IC$FQ-NvS_qBlqTjrRoWyJ1pHFfWO;VtI9&NJ;abi@QwAuLX;WbBoBvhJNhH{zyOyo~c zUa1+>vAk?g>*vCqDFNHVE}pUHo?OGR-d}i2DfiMTrF}j|k1t&HKD>O2WRcjN+QOZ` zwR#PYu8>ZfFh6z1V|m7Ckri?cW*3*M_LSNLZoGO>R77k$-xJHuh?l(%S9i*HHB8b} zs1?!-zA(W-`Aq1Cm{q<@7hPJOyQ$Fk$)tOaEY@0o)iwyeyUoAs-?=&4UO$g3zUy;G z=<>Z-k=ucrIQV40>~6b%Hoa8l!{ea22cI}T({`-YVLUF-7^mbH7itkEao6zqtl;{? zY7bZ5oyjuM@bIz2Eb^v_K1$r?EZG(7jyx2dIS@0WgMhqKjYlX-F%37=(;uk~H~v>;~Q%=1^j zbFqBBKUZqD<3@`OPtBcHeBx|uEAIB%se0h3XIjAR=Fp>ZZjnc_pOyb^jnZBFbgEL@ zbDi61u39^PzgS}Ssf~L}*vzTd=M_E=yCt-AMrBTB%|*{##jrb?#}>Ys-z%D1V7-lf zo<^wjnP+!Ix7j>;9?|-|=B)9YC(pB1?uop5ZlUmBJ)5)Dg;NS@UNmlGZaP?K9#WJw zL;Cu1-u+RX7Hijta&qd%tqZT4D)WBc)FOs((Kk&Gg4|Br+m{@8Y}S{G)|9oa(K1&G zvX1$Fjx%Fe%d}$^bCq&x)Kd2DOY0`raD9y9vOM?oqpkMAiE;Dy?bx#~=1N;u07FFJ z5``F6snvWXN~W8d*|R$DT;C+bx#D@&t535+=J14spId%uM{yL_oQf5jw9h} zK85Ske4hHsv%DW99+m4hpJNP}WA%EE!2<)g!-w)AyWO_>`}o|JoSh=8J0tg9KE7Lh%f95b zC3)JLTo3Wc6idpwRw+AkZEq8aO+l4AjDv459Tqp1&{OUaO@Imz$Orp)%>K6Z1;*&p>hJKLysmn6n z+q?16-y;=;TcavYot+TQRT-eUJtNmyJ`dS_k@ZuA+N-yrOjrFY<0Q2m z^u#$_kCg6+x2t=#xx*x5RhPN-tVI{*y7nuE#(1Pm*!Uyhj_TtAmqf;vSD}@rNwZdj z92MnR9rA4T1g1Nyj^0~Y(`NcM;O?Za4$|REO4m8J^!SUCxvWRZjNTjo;H>ujOCf zoBMoTXcwQwoXd-J3kU|rtmy$SAHGTZ8i7&O3|2YtF}wczL7lVBBM)| zqaE9g>Q#x#2A&gEFxD&*IL;z|O#7fp4_Dvpt9MVxiP;J7buthOxF7JwH_IhIQsLAu zy|)qaYi>&|d&T$iEAPbZa=&J;FD#zCOZMR6N3HszNfRFM9$YV;(EekR-X6)G#E%X) z^=kgyEIxm3U2{mX#re6_)tpU%d9@Rac>UBvYSjx4?TOWySdh0zny+WkDUI_oiyUm! z=UQx=F{d(s>!5_D)Rc=KzE`m1dW9);w_Mfsy~tN&`(@L!8`E^>x0)VrS8AVT(^PdK z)1K3{T16$FvFwD*iYB`rt}P3E=G#sEw&3)f;M60H2esQcLnV$)V0gMO``pXE2NyYB z9hT?%B^va!Dy&e@Mr;G~tz|P(*k{VTsC;9xvE;+!n$Sziij1{ivpp^eL?jfcZQx{! z$_YE|DByZX?Apicd9}P&+oq;$Pn`QQI!bBVmZxXBlRMTLu->nWh{$vg&x^l_2k#Dm&&Y7VW$>W3j*Rk|zRL`?(X+x^_N%oAmdO|2j)I zhuDv^6HeKFNf9#W4VV|p8+Vh%>eK5kb&Y4U@7_#uKEQFuT(v-WNmIe;^ABA+ zk6zvFtSDV{eyLk&&zbdaUft*ttBBI{>rZ*m?DA}%iDhf|wwpa&A674#)Ah}~XL(1S z)z#}&M|W-NtKPn=P0PCB@x*&uE_ZldIXX>7;dYT4@5D}vS8bhr(FF~ee05v(S$A(u zej~g3snaCJA2F-@5{y+#*DLAFG~K#q{Yr+og;`C{EV*_Znzv}5YBR&WOP^O7^D^u> zankOU#Kf5s@4J+K5@?+o*?IBJJg0?R{t8-*0drog^oe5Wx}m(ZX8I(Tc{~+6=L8zc zn8{u-VlcNoD)&XW@X>sbWFzlS?AOrmmYOEIJj}#zw2`I zoZbTN2o8v7BOqinlXw{6l@k9*nr^<2lPXPf=r-a;bm!U8|J+_Tjbr~o)zn?v}B{O-kdic3ZYFmXT?-rImDqF8$F@w zns?jGLl-iytGZl!rrlceqF(Tdl}zY1*Ci#Bd~Y_Y+ZH;1w7hwkaT~|+BbTK!4t>w^ zlT2dymAd8B4OT-bwS9o6qN=QWdhp=cBT~<3ne{9O~IcKYIl}kIxKO z>91u>6q)>bqTk86do>z-&DkS3_D4w?d~jRYba(02Bb|c3jOuK@tz8Sf=QE?nQ5& zSuT`%=(wTYzL(LYD`u|6ZQg)wlRTbYnIC?BZT946#i>i;ue^?Ys$aeGxMfO(3Nv%5 z-PUZ+>AkyVe5n0mo#K)5L~6-94%XU*+YfcN2yN^*9<sn*Q~w!e6D8A zgWoT@lba+fn`gaEV-HqXccD;}kvlWb`o*zTO#kj|5>-yzrg2W*=zG$--Hg)}@~xh+$cU*hL-l7W@+)^|m+lUeC0z_Up2f~*R$S-mxyNMqarN|e zgC2&m`;TMFc!G;B+PyY;yhYbuYp^;vcvn8#{8Vw$gT?YfIj%XmJMQkemNjc9 zPndj%MSsWXh1+!J^y>tCbNX)i)`){Uv^(lT@vf@{7npvf&2cSibdvLa^Lw@Kq4#k< z8c%)i?0P?UrPrB)N>JA(=SbpKeb6Qbd}n^cLy0hugZOz`Sb$2)m5v! ze48iYob0nE$3}0ObYst#?*-oA6z4 z`}AhTAnVg@`ZlfV-0L`wt9?G|#rb^y&qLW(i!a_?X|u(s?N7gpAgeE_m1*``W1Bs>@%c;v>$71?lXDsd*^24 zjJHxp=Q@g?54>|(E`CIZwOgo1~xdOV8gy*Ui5h-qEpAzaUimEopbQOxxGDvwDpl zo;zRHEh-ar>4BPnP3&`rP5dH4ydJMSrRS&iyUAv98zw*ZO?))HY(s~Q(XF(oh(x^O=90$IHpHru44LG%}p?!{4W5(SnRHeSy<*A=!-6GMqfmDOP@--Fj)2 z=xwRSV_)*?@*bU;z_PQUR%P|q$uq9Lb~4O(r!>icd13I2N||6w)u{oo?p!nSBo`H) zPTa?;8+MVwuPu3_?54EOQk5Agj~P}*PyF2Rv0?37u8y5O9xJoeX561Ow|~dHEjHg? z9I-KC53;em@%YECrAm2U#aZMZtxlS1z$rG-aCu?Em70d~=j%(YLaWuq&K=lSnxP{4 zF=WSjjxBw6^K#>b~re0+Ao&#Iu`$9_J@yslQ!!B=xMQuNM*SS`zIT?`48&l)5H z#P+Ux?y`5^l)+NzuyNg&(v0eSda4vdLQ{6$gj~t6Cc_t7*O zNlV4{)opSb}TkG{5PMme@`&w`yYUzgT#!^qt6H48}Gh!0hKA&Y< zE}FBuHStQg4CC3%Wg^e&KAvXmx$0U_w!S$x_<1ao&hA;i>_W|+2F5h^P5t~%TeU=5 z)1~ODrk~%{keIVxZtD)+_MO79a=Br^%4NrP1TyuW*V4O}y5-?S0lP`h($c?1v`zhe z<3hRa_L++XqHVWM_Iu#SwQs||Yny&;`8&0}lb2od({#7=m4?wfE?w12xn*YMB)EIt ze#VgQmWf|kE^yvH@w-*$k&K^pXNc1hSBc9~b2mDxyt}l>SfIxtr}&`t^=6*D2tDDw zSqYMHA3q2$_+Gkt^V|aqzq)wHOf3sxPGHyCA-l8o>w_KPYkTq=v{>Z2zx`(r(LTAn zk7@OilLe1oJdDXsyz+k2`jx9@NzXLjG15J{#`)~9*em>@Zt9*Uuas5qO8}ef(t#R7azbJL`MGy6NE4-Q| zl;yitWh|WVV`7n|fkfi3)PpPACK*0b%dp#4K9PN1-HzX{RcB9kytnzg;NH2nS#KHd z>UR$M;QQF^qWTT~eZ7w^Gz&~f==jc3q;)U8Z%T8IK=$G}>M_&RIUmkaj0$)f+qL1D zvC9^IrKS{>%7BwfrOSkCSgUo}ZR+B;bJ8kE?yt1|`IC>|~H6e-H2X;C4{1d$WF|4BUGDAS* zG0WYCD!&*Q*4b_p%wBIY*|=K6ZPvDzL6d}<5AR&@`oMwbf$#77&n=Si-Q7Lug2MN} zZ%<~_IM)OSrFeOLW}6kYB|q&hGxWMXg>62TStBxKo!}C(UC;r*f zVuUnR7A`L8JrmaTtK?!wVdMF)tL+VRe%ktd$(^yI$+z)@;`K&`nBGvSzO2HP$u1^^ z<<)ujUVT#K()h7NVcNl+kCrX=nxK(pJ!^j`&%dh$+*2>S$ogXHBemK-w`$3=t=uh( zraa7&IrsI$iKriGJ2rWJPdv3GTDxn_&+9vt_UzPB+sD%N<6ZXQ@8Mygb6#Z?Xf1sn z%(lVlg7#g9&+DCY16YboAM=DKR)wCr$s)C~F*xqkdcLo3rY{TeTs7M?lO;6A?$?VL zjnkZ7KP-P%1(;{gZu@>wZ0)iD{zI$gq}2zNuQPCUmwlLfEYl=U`~S6LTe4npNHRRz zvxDo}1I7&tuf05Q;^}G8Pp3Q-cgi^}NhoOEv8wgaAE(ZOmkkE4)go2mHphO7K3dh_ z@$_lf%Fs~e*SS$@7sMDQtA@FxGSq(FuxJstOvlSNk3~$c$ewyW<6Lo2+fyI!)zjw| z9+-AwhgmF(>Gh3V_w;x+b{@EErZly4E91PmnP>c^&m`48*7kb3#lTmEC3mND)Pe_W z%uA04osHBxW_o_DAWO4_dft>29>(L&o@Wqxe)(;%G*0ta5i==-fpsGupbRt`a$D6tMZj zHOw(&)1M9(*z{WMR%bP192`XR41%20ATzK5rsJq3fz^$4xvRG)6Cv-T7r% zn1#ARjb@qM^dp9Ty2>28j{RI5u3&c9L@LNXNT-(}c`d(9{)AI?qB%X1Zoj*hS$bQB zTLhlCw3glW#5-kG?v85_Ci|{z)b%NP>fU_))T}c%ETp>xm!7+yD>r)%BePP=hf9tD z0&O-M!EAR$qAG=W8!p_I3wF^BLzPs^$-|m#|x@gNbXy&HKnknc=f!pm2*{O-W=NI`*zg{v3*@d z-W>;@+$$|>RBAmZ?dN`MRh#KFvoOg|>(kPGRf?HCe*~NmW;O_9RPa2}A~rp(^u?Ul zH)c#?meG&erO9>6ujJ{B&#RUC4D)8SceZ&h5*tn@BkEwV23PZV*mQ0KKS`8|{m`1WjU+R)B zYVmEFbbPl-}oHA*R zRjFj&>NziZda`mu!^-`~B6HUU1m^F!}9DhV*mY?+@*h$bF_YGopLJ z@604#-We^ukHYSJjz?>)4cOOP zHCy_uk1}vvyH%j%`8>1rc{!Z{2lDJi6GCztN~53LiSh{9^t#hu-#<#ikjGK9Z~||? zm*kx1w!WebcN3pPMm(7FXx-+L;HA5+PdL6#>8j`RiE#!GqW0X(OjAdvX81R->7o zNktj2D;+y4nKah1u4OKIpfJlT%e$;uaj3+}I^`smRQ_-|BZ;%-ZDwrKMNeZ~VBH#S?LQ zcIN?)s6^gFj14nAQ@WMbEMQBV=yE(;CL&e3>({Gi(hFDC7`eZh#rvbP)7$RG^H~w> zI~ebBY`XKw&#uTM+3uJ7(RC-z&Fy&N%Be0Bsd8b#`k607K5`}-KC$zZzPK`=IPQ+K zY5U%#>zXIU-rsfV9K$t7cDt=#r(O&*oGE27JIU<LpJlZ?3_Dxbn=PLVz}kfKE0ycLgri4M`fF2>NM!vTdjL{O`&D}15?+vhgQE%Zn(4gxpPeX_s@TBhnZHp6pOH3uG9A6 zUL?d29u;?x@zUWS0q@Lbeua(Ux{p-fl>L3c@tbo-g}qCyp{MeeUA@B+C?LO`2NDb+%P`!ky=*7Q9jNU`yBe8!Lsemzax*V=j0Yxn!k1Ywbgm?QN4DF_Acn6+K{n#7$T-@ir|OHxy7T+9OhPTq0c`FU_(=G$k-j_Ccc?g`a98X@PB zAAVx)6R*P(b|+uI+PXZ?QgnjHj*D`Ao9^l!aSTn7D3xYne{~^zr?}7Ko5>r$=XU+N z^2nC!fMIUqi)_!M)^^LT`V=41zro2Pb*x?TprPj+;b**h&&{T~o!fKYR+@X>A&1j* zHl5X0X=gul=eU=eb>=&fvw~sDNiz>8p9wfGot<#HO%Ugu$ zOV2-)^W0bR2%|zd%No-qx7;&Zc6MBClSn+bOj)XU*1qTMQ)jz`3!eIMlk>vyx>?)o zbNPNAtvPFP~9$s)w@#PRd~$$67VE{Wio3dGqo#aW z=k(->_#}li6&}8N=Dk%jq(h$ew(i>S^v(R%V99ka_N;xkd2hH*-VBwT+g_v^@SU*e(^cQxAv@Sqq^GeRUbDg2)Mdz z`?LDQH|gs)&V4<))Rfs~m0tP!=KMI&Sx_&u4`g|jui%<+LpV;OVbya>)7%wKd+)4X zC*+vQ&aQ0T{5-^?=!)xxdz=1sJ@&WcEzJ11G4=Ejzq7sn8GN+v$qGz*%6Uh<`;ld^ z#}&c7{2GSd>+JdGPh7Zdn_i~)y7LrmV;&4D$u*mE zR@wSpnH#%oYWRo#j;hjSlJ8X;JZAZAR-1hK0N;OxY_7|$hd#RAj{eSZR=Iq-P`=6X znf6UKQ%XeDZC;-(6|)X}%Inx0QaEdNs89Ck>^tv+-H!3cTP+XuFgW-1=u_?$5yvjB zys|?_c-`c;8h<(MYtv^s$*Sio|B20#kQCy8r{m_mtCs)phc^5JHnQzh^vG+$sthaG7XI?T`d?9=vTd>ID zpm~cwUYL8Pd0m#*WIku*Q=4VNi_a*|^vOF>eyQ@JVACmv=O+RSZydK>bJ>H-#MNoO z(ebICx*ymYH>|f>U34Y=+78Wmhq`w>oDhAZv-_D3&+qeIzG{C~?Cw-c61*`r&Z)IX zdx~?za~YSJajV%C88}>y-kWWv#W#V^e^;BZ_j0dE>{BIqKX|J(`#lkyU&|plCCfT( z*PXkai>>vn)gG#AuiiTEHTzulIeVjGCU&iLlQwbVm^xjuBVKU((kjcOrRS>}Ek&0$ zEjZSmyf^0A3e#n4CZ|5jD*U`6^g{2qwKCJ(G8&s0*3V!Jd>Xf1OX^w{%k`G)dsZKg zij3#_wRqMSsZWo-TddSP>$uJI)gtNyb=t!&`t<4R+<3mWeR-iTz7@yB z1tw1p-B-9ZLOf7yNwVmj>6=5N*QH9n44%S2r%r2=$>klQk*9qpU!Gc(=g;C`xu7uq z|Mh2oe>g8cx9%EKQ=N!Jz>#@d!X{nN>dW@nx#;A!9Wx&+n^fV)_BZQghu|{H3mXl1 z_Zw&hd#(%@ZMs(w#kVp1yta*$bm4R7qm5aP&v>I%dIHpALIaOgZ{w*gjA4@A^=a25 z&v{(_dJ}flF1*$;FLWyN_AT>knR+}KuCC*rWXTw-(_}Ms!b{e$jhrgiB`+2471Z6b z%31GK<%NwWl#^fjRDZ2#jNNu`&mV{0(`y-;E!XG9H0`wR%Fox=I)BvQWX?N&t^7=l z0{*D*jI@<&=Pi9`Fo#V#Xyz52nNNM5)VM`_Vwwuh#7VD|cXn3~Gr zzO8TiWfm;v7x!8r%qZXNekPUgQ$UgB>6(_Xg*~CUJ8w@7R6W3AR<>U3VYG$mL!W0` z;zax9SoL=JoL#?S8C&4uqt2%Ux67=Ua+2+@M<|2R-K;j-N0U!n-Xz#}BOWmHv zJb7NGwl2ChcZ!|jxrK&SOpgoaTyuQx@ita@HfzX+?FGr~N6%b8VmFn2xz!Y{GdG#5 zzJ*Mk;l^%Px^j<@`8vi-Sue&0mGtCO9fHm#Q4v4(ya-qr77_p1^^Vjc%UO2mk7ISz z7bUcwDNIre{akQTKrDEIRX~@~t?0ZdRk32Rw^n6@xt;2L?Ni;-{yi+_*zCvMh+##+^`BeM_(WUEr!S3X{ystC^Uj>=71ABNTYjip z+zIA+&K_KNE|KxirCYs{X`PRG@12QWx2C^v;aUe{jh7Q=?fdjljl*!?#7C~@?T#Cn$`hB)Nz0hztJ&ahU==WblJRjl#+QxWOJ^=oo%2e4 z<&umy=bm!k$(g+}a^VcOtepnYd7;4q(ac#ha-Pd-Equ#!RY8bladSpT=CA#$o#!8q z7qcq2=lap)Alh)_ZM5FJBe}af9-mmXWX80UrnO5lmzL)5Wv!R&N{?TCCLluURZCXS z%UpxEqAv{tRMfMQGZkhjFTW*yX?9BH+Z4yQyx003{F;#Z?M1ffEaup~OS2~5iMe}@ zbLx~!+H+&BT#n3R><;|4aWB(~UEcapJv~0jvRc&#svA4}x>-wf*I$^m?y2?sr3XHI z77_XGB*Zm&mel)B-7{8xXDokRjC6g{#BTDe<=0vvzA9~Z;nK8YH=hR8sa;Z=d_9Yg zyY5dbd-0`}bFH_ho-6shtW_)ZqBsZNiVrGJ0uArX*xWdw@`~~I~@ zPbO-A?(A!0U#jq}ZRfh|HNv?&6uPI*o!I1IV{u@`Sp}|}cS;tmTX^&`PY&mSh0nOU zyjLpc1iw*u_{z&hP}yNs=sAxUthG{256n!yo}U^i?eU4NKPUXG*z$u4#TF((39x;a&mJk|=c`#_|72CaWaK$}T78<9Sy`~* zxk8C=UrW#Z4*eXJ%{7zB^iJOvsdY>H6lzVmb4?ANU)6pmePkVvAg5d6dsAP1B|feO z{@4>HQ6F|!2AQ?pigpyQnEZW*>Y4Tz^SLkARlE$H+Oxh`rQT`vo9BK{-p=|a9sT0S zcXQ*FZKCfM6~48$@8-Eb_bkf}8NZZZ>+NUTb1yb*;M*^fq7r@M&(kDv4%K&00xVZb zT(Q}{^tI^i^Pf*FewXgkz!4tqWZ)RaBH->-d@!x+eT!G_bhqh}ZdM%$42j!)uD3~^ z(K!~eV5-Lj6|3U;^4Ib-o3J9yGYzg+3lQL%#S?^v3iArBtT zmi{c&c2neVblmF4Q%vsZ?sI1ni#=&LDMH3r^UmGwZ4Qxfp`Tcqe@$uo+^#!iMr3`_ z{f_~skL>8?)h$)CcgdP5Id`k^mi7;wXRY0)T{vEmnv-0js&!?-v(K6b?@u!3wMi45 z=ri$?eCvwzWXGd|!mBI}p6`(N+H_0g(tDMKhHusx`LTPcNk%p&7IECz@Y=^sk~w5= zkF1l>%W0lVD%zBFN|?{+du=)wdQa2b?dgkqrP;Q-)H(yQY|d1NI7PilzT!V|orG@R zqi>t{`Yz@>b@Os#`+;+|g+6PS&-!*r;z@31?rMeyOq{0~DxXbtSJwW~#{TM}VT{D4 z)4lqeH9mh{xOel6Nk-Zw_l1nuO*soDiB6c-k#bzYx}kQ?h4a!b9O{PK)kFg395IZIGW=})zw6Rb7dSLgWZl3(Otg|&7%53|NX|KMt z*W#Gfm-US{tB;+nS(3Dq`*P{KWBZgvg@f1&XMK9JNJ&ch{_OBsjS0&i260__m@KKh z)HgRp+;DNSj=Sz7b5N_V;@o8P_wYlPl->npE3}Ba8 zb@~v{>N5& zUh-46cDvap@-E=HvL(-x(E3RXjlORp;TPj!wVG#fIAo@1~erJ;;wT zot$&!Yg%)cPuJe)tP9$wd_y*hnq@8R%`DOPTyOj>ggboZdc8%-xi9<-bJq%`JYDhn z`vil!S6idH`h7NT(W^1ywY0VAKX9GT``YX$n;T7ee|G(he3VdCdG+$K3trAW0ww$R zgj^LpW_RkC>Y}4HPMXt$9^1P5?>hD%_gdqT4Q5N9Uh8Z6DlgT?9~h}z^l8$uHQHDE zJnjee^lN=`nHv4#dX(&y{-ePiUK39m^Qwv~KPvpSb<%FF)7Jx~+}dsK`MX(2)k&;= zBXE{8r|3q|^yvY&=bCTwRNRoJV}4&`Z((cGcfC(xFX#9_V2ZEJ6Y^<0VR>u1_r}%B zr`~eAZ^{~~exyKiRl?RqJ~tVkSjdPPwf|uYSn|&6vK) zK5Jp|I&Si|>~_=lnl(Gr)s~&%J3Q4vZt6qpM|*Y|g&T%AXthoGvU2A+n^nsKvJaMg z2s>(KW@^)0DPj@3IqX>Z!KX0=(V3Q_|1Ugqc~KM6y^HaO+Uf~&U9QYND)fA=rlgmJ z9}NaHBw79vF*o-Hl5`%uO5}E zH{W{g{4dR~s`oYTjz?l0OusLj@~H3Evexpu)3Gqo&FB@gQm50>&8J1Y+V_Y>K3Sj= zB-@phS+DO6qM7n6Ck-t|M(WqyUbdIwymPV@?SAHLvI@BPGPmDqhk3agzj92$#nR;=B`M!)U(J{L{5@3O!hLqo+~bB} zw|BWcwqE>MWY;Uxwx=^Ig*+A-F4)mLCsOd80+V&Hgc`TnG6}XN-!)PXJipY?^mvc^ zpBD>w-l<-nyE$6K(DL?y2-6L1?^KO#y5Yo*?KeR`FBt#9c=m9Hz69%LUpS`!{tbM8yhlnMzUM&_wJ z%O%xot}&mS?V=>cD0S-NTtSmfP4!_*Wx4_;w;niV^J-DcbLHe~s~oJ)s!z@4P)rtN z_1 zL(VHRy?#Sn)4srv;a3IC$%ott#?7u;|CuMYm%f89%UDQSo_V z*JYVI4BM5&Vq`m-R%>0{X7zPd#M8}}Iw#h+%bCHtOuv_c)f$V8g)8R1*zq)2 z^2Ggf%~}T@bnO!7dLS42yum%_vy9{GFqKb=$I4#D-SJR8n0sE?Z|&N*sg4iWPp#hl zVB)mvVj*(bUc8H@SvOrW^_>-J;9$}AWPjU)*3PFsQ;yZnNnfY6Wur?>@lj6B)ZHr8 z+FKJp2JAfWWXs892Sw|eW2+dSS%!Xi`R;RoPpd6?-MNEwI(? zjOrh^Qlni(3~#-+3hs@qowlP)?w5>A*K*IsHL_l{;X?0-V+%%_0fK{ZVTS<7!^{d9D?#1R*>SHNTk`5YUfb|PL8|=F(_MlZ&R)4CQkf?jORk*_dDN_Ss>XGq#b!ko zr8!w&RzH2%Sf(t&X`jFYHP7eBv6MD>y%BpEvLJbw%rnce+b- z3+z70aEBpn!UPBP^HaqSD>iwbID7pW*G|bNzM@BRlwGfwx@6;)X8Yv0CBx%e3j%X5_a00=eDHwP-P=d}Ce-}>VlKZYPVs02+b;>no?nk| zt!bP7R^*Q58s4WIf7P5`mPxq?cbtKDb=GW|;9Q!e9+~8EF=SPS;#MBP_4$So$r{_*-{} z%AwFK_@q8)Z|@nOz_ukdDS?R#(o(rZrDx4~5NZ^?c~U~G$DR+xr{?kRwNovWP}Kks*C_~wGyyFDg_ z$9nvlIJcbZMM22#V-n%qvn@RLbw|Xv3y59i*LcqtE-8Kee)P0|rH@kb`0lLoxm!Hv z`ZAr3+BXeCp3ikKHWT&Zx0=V@#BO|Q6}yn&^Q{%Jm7>Cn*QLZdOC~;Zk81W^xra-z zZiT2s-*j2=iR&%Jjyn~`bWFXyS}yGJc`3iEoT_O7#@na52dLL~tdDe5_K$kO9pv>> z+5L6%wY9Ocy=CTk-9LKOdu2S^-sE*YT_0n@7iY}A*f#m&stFA@e@IPpTl$$LZRNQ% zg*TkHc8Gg!2%2e7b|?H4e|g`|mFa;8*Kcxox?SgbYO2AO5`zsk7t-2yWISb;d3{>Q zXHD~DMG=J>LlLuc>U@i~`J3)tSTU1#*%}quRVX2T{5ugyt=7N=+iw8m!fE|O9$25d!2S!Jq|LPz;(`TM7GYwL(BduOF4c=N@T z1~tWNS%x=hrj;wddgpnoO>^tcrvjd5zAc(QNk`wT<<*PhIT7Llw@1?@SX2z=9AMuN=0)f%dTOv7A$19C^TxAYvS|zf^b)xpT<6=MtxPe z$KL~7o;;eBc;wjJ2O{h@j}*o|TRM03*_|Pedrm7Yc;=gxAXE~YmwlAweQ>UAco zJEp(N$(qR)er?lb1=-hy3!Wtrz9`}=U!-8Odfmy?WiPf^NQa+STex()oZC_< z@f!i(1vh!_)nU-@+-0d^>f~MeZVBf@maNN{4?kMET;ZwTP$`}b_r z--}-7Fxqq3OzY6sXtS(EUxgJuuV-DFmwm`LeEqdO*Jr)@vf4R6{HPkog3{1VHSMES zp?e%n{m!4UV3+5S-xI)WD7!uITUWAnR;%=a6X)+cn`GVo`-QVz@Ti>Xf)7ElLhHio z4MRNhn`&0S<(JM1ZhofqWNyl#lMnOl+IC;rRI&1=x00&{%UtCE`M$1c?<^~7)`+ui zS)iW#dDZI8-N#PU?Kplrq)>SAu5CSuO0oYLY@XlBdX~ax=XBD5KYY^ST*0Ydx^h|P z+&$Sl=cW2o?T6)b2jvWk#>e zmt0;M>$D~^9Xj)pryR)yOZ_0+8GjK`OmUc+!VK){1=9hgXM<%2ye8;j& zm+u7!F+YE|c=xhNwpnH(J909&PL`i-S6Q;)n(6|J{FW+n71^MfZStbBQDI99mhV}0 zC!uKZc7>+b7OPGkdp z=K2|L<9JHft@DX=Jy&)%r1EM@en4Urx2i|ct$pj;4()yUIxZ$YYh~Mg%N6`ylX|vk zcQ07YoszbA&f1#e(p(LBr6=O1zIc9a-}!D+*{$I{<)$}c!kP>NEYDVp8SnM{IHB&= zv(-rtE0)*Dh1QoX^yW)jaezH*w)w@EvX!$QN>96$lv@A);xnT;E9`S!*fl5a5KT#t zxNZ7GdWZ9gOPd)!xH46TBp+MAD=Ift%HJ?V#?_L&@X~ts-j1-_w%Pjo1EW(X3OqKI zoa*1YPt<2!LJse7=YUEn2Z>n%i8E)5_#`PAg)2n3+cagrG+3UyZQ<1e-?ocBX`S&( zJ@M4`hO?(m)~*pOIC^sS%IATPR2FN>bgXdIEWO~zuzuZXkJV3hhdJ0Rn7ZgeWU5P% zXkswC(W|ekb;HCG@A0-9hTW5{Y34Zg_0qdC33JJHi57+)8&6879e6auvuQ!lZZ;NO z6Xl$~kk&+gzl%mkHa!!XlqJ*aC@m+IYLj((qW6|YHD1B~Be!2{_?BpPHBRY@n^vye z4dtv&Hay{zPq1qpQ}WSuU6NaUvQh5ntkQzM zDrIBi+cX}va+JP2w|ZY^N}x&d(XXwQeG)sYPsUaINnWYsn(OR$U&hSnWge>*PiXFXsq#p=N2fdSk?6-JnbzC3Su>ZWe2}Zn*ldggD8$E5cxvTN)or$wQs0cu zPLXN%`sHeQsVb?B#WG_i*NVjzf~R{s7AD0Owb;zK?CD?08SlF?sm=Z5OV?#D`P7bb zKH|6|7y-DATB>c*FQIl ztbV_~`Fl%(KyKVIBknmf#kFQUJb6@>ccabE@QGRi{81VL4GeWATcg?%)G{v%YP|OL zV5*!SrLNVzfQQlR@ovLymR;)49ad&}zNs|Io0UC_b}y$hZdg@?e&1Poeky}@NtC6*-P58n&L>^>$!-o2T)J)j#FKW5 z_D*T@4|HK@=Ezg%49MhMA-f^_?6IpapSrYI`c#y@>^+dYeQrkb?7abwCypjh{W7uH z-y-)S_mWG}v5c2F-%eZKY3_Db(aCLA$Z}!NXP(Pl4AUQu>r=qp8Npr$IMn%?Leqb)q|-QCy!YM8u|L!9V)FWCA-(8i}S2H zJoElsh0|I_6F5xwKPnT5S*TLcA9!xVpQv-*p8iIArB=Q%o%OKc=1JX^%a<<*+$Q*4 zSJ`vv)4L>7AY3OhGrc@?;}(;^e*QNvS^mwQhG0WbgKfy%lptm#5@d_i4}6?K}D+{APwc^);UN z^Zm^ybDqp+PChmDaMRC~&We$DtqgD9_ubiBe0V`pM&~u5#CN=%8;v<;dq(knS3j~_ zFKfjTAv=#tHxDXbPY$20sjhhN`R3}Qh8y4PnXY_TZQAE!u8ChYgIRVQS!^~p`~cHI zZN;k}7+A~7{wRhme(AH0>1V0kul=0`)vwutwK{|CZq1S1^Fr6%GuEp7oaNuI&-Pfb z8#D>ryKZv0MQsk#!(`8dQg+WUo~j9@jDKXa?y1e#QO0x3T=vt$J?svLpYWwin(Vsf zD4lgKej@v{+x^?Rrmjodn9=oDd9^H`n}d_YuHRP+KP-4)v$HNZ*2tTqQuDY0Qb?;(!@83KI9?$G;Y_FDBJuZ@U3r|}6Ysy7N&3V~3Jg&?<$0=)@ zwP980_HTaobHz5DR4Q+2k^InPcKcG>jKA8MH`47w5A0qL)a|Uy_KeR_RINYoibVcS z*{lsxnkA=9el|a;l-hV`4*weC{c}spU4suR+~HmQL|H+&Yhqujufv383se((9=5No zI-;jEVAd${r7E^QS&Y?WZjc);XWzz+^I7^)IIE*dxCZc-#O;(Xjtj{=HR;0^Q<%H zDNLFgJLNg^-4LeI{7bVx?s(|^@vdwiUzLErO#s_D&%85%Jkt)IpJ1CYWmVI$9W$n# zz3^(2+=JT(yR~-|dAx6xl)3SfYulG;VcvmX9JlQ5TiY|y&2&NA*@atA7Cx+Wl)G*d zy6Mmy>&<1a%x67wzf$=ik5}*2rEe1}3zDVp9|?^1*}5S|S-5}7r08WUzVan}T9kWd zbz9t=*RL(6?^cmL_QqB#U~2V?2fkNNocMh<_uZUiN86**I(Q#BwF|4{ESnpryVnYxwSk?kB4#X5lpTjQFXJpUa%#0+%-~w1S9NjP-D?I8XGNdgzAkaoddjt+2BSAw{K{TS9#>5^ zn^ez!YEPeQs)fYT6HFg#U))iZPCl&UU959M@2dM7n{DE3j;l1hbDtDWa1Azn^H#ic zeO`1x-g(}sXWSFj`gg9r67k3Ne90siKJKe>XB5}uHtc*fecj`0b8ju|ZacVoc2+dw}!<$+mpWPbk_&&C99dQ?|mA$YTwn^ zn5lKUCha^MAhldB@&L2S+}48iUfB|7IOi+X{aX0wmF2~SOd1ZA3AIzBb^Y|8Pcqfo zoq5!3OHUJv+2nSKvsD+9r*EqH89GTp;rY}M>lfdgB<&gZcQ4*@Dr~XAlb(&&CNZBa z$dF|AFPv1nxxe77q|7Vbl*t~Q#|{TRpMOwft7_@Qh~nV6uXAS~OVs*2<#p-yZIjOG zDsSE1;l{w^dE9o_wd~$4O1}lY@4bDLA9FSTNFJx*>W2{(4GZKm`&jjz(~GmjciwEy z+v2s=F-@%7Yqo_-m+Z&6t1fR`{pQe`yDxpcESr7L3HYQ;?{Q(?%f@+Y8Q-eh!op0| z-TFl=%U3K|$D*+!AhF|4Sai4u(l&W_N1*?_h^qb z@9V49*TbjZJm#=og(Y3|`?Zz3dYb&LK7H#xP{#gtrR;$fn_fEh_f|x6?=2Ef6WPS@ zb-kxm)>f$*>F3r=XmXHdjJsNrvj6|3=S=rQ^iGw>CA9?|XucAx!el9B_ATW}?N{HU zs}3FXJhyn`5~<$@udCc%`O4&MP?9lk@~qG6nb>1KDRmf1mnz%cSS299nqlR-TU^Qb zZJa=C$yb@xPuSfiZ(KRS@=DCS?DK1@O5$20Pgq|$T~V9;(!fD^e$<8|61f}KJrbKb zN9~m$@1Iqd=LlpiSian9hsunTqSIx=W|)Mo6S<-`SwiNhZ+pM3&Fa^)OEuS8_9gA% zIHDeU*dQ@BU2W11YpHUXRmsP)4fA3)FfX;cT7q`okrE4)`kQs_ z#Jv?0_e3t(5G(oY)Q19%?3sL%ySB)zw_dSUA`W^vv_A*KXoK{ z&kWC^w!mGV1JhQ8p7)xz=DNY5Z5_`dE2)z_YhpG0mUn)7nBo}Bow(F-X{N-ql}cGTU)D?ev9bO2 zjcL$0@!Ca=wMD0U_RM$>C*Bo7-gDcJt^YQ^_V`q3-feqi68M}p#c6hGC$=rz=Aqg+ z(P-XvW0zaUN-K6=bdpt0+#j{@38%Ui=Zbhmi6EY)y^|jFdhu^6H!;1aE4J|cs%I13 z)*niqQx$&6gGX7`@}jgqXQuRpxSvNX6q^Oy16>c>U*+EJ=#;jmvFqf#ww{lQtbOfD zPi8PL=?VBGv%$1vm&!r;+=|3gZzD391m!qq#2wY+d$wj$Qth#o5g&@Q^!ye-kI~IE z+9UI9t5E;S$}3V2+YY=9VRE>&c;~%6nw`78Yb5&K?R#>)`KY9~`3mMUSR_p=GiH&!M>_n+NM2Zv~-v`Y5v|1@A~|`PJh&%n|I^sEYrKQ zE$z9!6l|TRbmLB6$ySC_>cU&yS9U(;4zjLOR9&;`RT=x#Y>Rx!zDK5)ZolE0m$Y&E zw@J>1F)8_>4xb%tDjj}|IU^t@DA{97ZW3+k{c0| zA~*BX(iL4P6Vq524*J=CU1q!K*_mlB$3@f)?_4S>^ZKowapSey)n|LNuWjv|BT!R$ zf4SEB)^ppgtGUHG7ii!6xpmL+1kVHCuXogIB+O{odgcHR=VZlq?s>7TkGxJXJ)aTt`MCe439+}2EZI}DYh9&$EyJXaFT3V1d1ih;OEu4D zV%|^PGl7|J9!;I)Cn7adv{DS)%nP4 z^nO0O^M`+Yp&^~hJ~0B~=a;ei-RXFz++Vmp!F_g--MjWnoipN?{=U2VamuRisb%MG zzI2b`PKnG*ReT=A5->l#E+@mhb6Q$hP|2Kim5)#S>T_Fb$lf0^sa57wgQwgt>02C~ zooDkVDqa?r(ztt6`_!f9dl)y|D!(l!9G5A@Z1>}~$8(>GaF#uLI^QXlR%;v-H#&A; zm;SuIbeR{N%eJ@9yQ!qOI7wxa^P`+1izkcM7TCyT`zhUd?6-CK#>4Yov&Z=D^*$-& zytGk;Kecdcpq5#2<15yP>5~=cK=N)!_$(Eyz{Fq zcB^IDv@lpDu$bX-aH-|W**{zwl{fd9i^@Q_A9_AXUp011s-ib?|c8k}{ znUce6@#R>Cr=a8UnKv$e7n{N4IE|yr!1fAPU%rWTq}wvtqPvRX$JZ(--TNf-;=-y^ zLRQ0=t<$7QTL&yhoobJhY|k(4EQmD&{#^zB5~F@mWXgk9qsDw>-6v zf8Xk##2vXlZF}*eg1wgn_boV*d47jn$@H!z*Tt`!3o4sb?cP#kqo^!p`A1vio>oY5 zzlm9Vt#=E*%DStW$sXeK@)JToTut}h#;haEW&3PKM8RE~AnT4R9YRWx{kk<9XT=|k zoc3_aTtoAg?Z>tV-ao0k^oYR`7XvfFUqY2(Wz(-@&y$|q!1AGV`iiYf8+IF{Z;92} zAFIbbXS2{6_vuTu_IVmCosr4xrTM(8RP^-iNbS1{Ge7xRK3?{0THK*Yf@RSGg}W-N z0?W$9Ch$2NC@TtIZIYl~`?yK$Y5-GwLe|bszLF=JFN3D1uoRhoVL0`wC0$+Q!PVzQ z{>8!Pg!40h3Tns6x;eOR>Py`7(xmgQ*@R-hir%v=hB{?Aak^6F5?T)!4qva^(lX)g zgz}}^5C2Y54@qLe}xnJa4@__^YFa8N+<+JH$$QYySz5|{ZNZMd}0 zXdBzAU*^smS8Qhfuxj(;^Iqo!c6YsK3uq6zYP4adm)U`@q6Krk9*d~7ygIRNOTo#~ zW32s2bLTdnESAVsGjL3v*IXCk`()PK{+XuFdGn?BKj_{2Y~_vw&(G^R)m~pas2u8S z>aexqs?0?OCsiNQ>Q$@GR&lO7^i4HGt?%K4ndw(*JN~%*`XZmJ7d+YjM+uX5s`~B8 zNA5K*So&@HgJsFa2{UhIewym`>~ojus%1~ZghO_&d>h8K%xlSSd9gi`<#r|e&q9Px%J9i?+c+k1zA>`3S$me z#vGjy&aQIxf=_sWHJ1qAR>Qv1=O%*OH#(ZupXzihe)wrsp6azHeX&}bin0&68D85X zp(1zmPLQ&yY4P%T+7F)O#wleT5KTUmT(Fbn>$*udd&60dtoo{Fki}DcYENQwT;r?@ zF)CA^nVqd%E&B3l*ZP^qqh6ib(%|M=dD-;ghnzZ7#pcr!*Xao@;=8fF<8@r)BkpT^ zbb{~3#$9@)W4O1Xw@q$qMaV4U<>}Io+8Lg+ {{end}} {{define "content"}} @@ -29,13 +42,8 @@
-
-
-
-

Report Mosquitoes Online

-

This is the reporting page for mosquito problems in your area.

-
-
+
From 9d5cc8575732d68bb997a2af5a8fc4b79241605a Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 15:27:04 +0000 Subject: [PATCH 0186/1513] Remove old RMO banner --- htmlpage/static/img/rmo-banner-1440.png | Bin 33292 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 htmlpage/static/img/rmo-banner-1440.png diff --git a/htmlpage/static/img/rmo-banner-1440.png b/htmlpage/static/img/rmo-banner-1440.png deleted file mode 100644 index 03b28559e2c681f3a36c9f2d6de563e4e2960aec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33292 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

o>J@8T<#W9|*L{Be=yrbkS(%q- z&t+J1MV|SYHkn)d?PAFewJ&ppC$44P<|b@<|KO$H+DQw-7BAwyJ=<#bvmN_bUX;F8 z-02uy;<-P=d$-&yJImYpZP)ZqO+EfQ*5A!j+Iqgd_|x8_3y#j%!#n?eU8+%LZF*AW z!}tetDlQgQRoWhBoawKk7MY@wVVbqkY4N&K2TNwFtSj7CzvyFypTqsYlXF>`9CQsX zpZHeMt@7~-*Bd8)mGejZ4Kt5C7TA%0O1S6olqz2awFOOO)6d>5h)-%`Vm)-DMV;+; z>6_F7b?*w{c@HAkYj$U6uRHcFqkBbRs`ihNqBq)Ef=wJ>HCNV`-FBOGo=q@q)(xrs zIsg3b#Z?qMo0;t@;uz?q^xdbjspv-V{^h$eZ&#~T1TT^}UU%rsRiCcIQ|*P1b_5w* zo~7V6rTli}_rxS_aj_#aGSB`?;7=C}U-zzG!R^W<4v}Sdj1z5DJ^n7^51Rdk(_!w? z^kwc!PDeT`>gb6nU@p3LRorQ&#l%4QOfO4 z1+z@o#r!qi>LhU9hdI>TU2o!cp#>Y<0$#4X+r52(ypEyK(|rdou-tzqJNHM|+lyv_ zhfkS*=(e+bdAhq?)@(j6lVi)u?l(Q#IHkh=&GDX>9G_@%-qhsbzJok7zFm8N>_khc z@E%=#|K(GEz5JWAN z%9quO#1x&m%FwR9kUKD=jhKg*Y%h8Espv2#wjDY zZ718e)68?O{xJS3prXlnN#%^(f}ALYD}UM6+|_%)wcE`0it5F=j-3zQH@tfF=g|!j zN6uM#Z^fVAIn!~5h3nfb@1(myQa zSZFSNljBA8$&Go;%ebPS%Bbqd8rs#a*cEBh`DXVl=f=$4#=nD;)DA7=h(0>UOZcA9 zuWQdHmWeMg*?oFBS9aK^mJ`b-%Rd&5V@8sTlbGkP@sn%Y3 zvv6XkN%z|6i7g+Zi*3C647(1`n%h*6wfv^KkKkU{cvzDhP zB!6?UOU>M7_IS!QP|67WBkh(5ofCb8qI< zb@%uC97@kTppaSo<^GoM3*{R+Ijr>_o#I?7dGDUx_bWeLl?_4{ZXqx1HMp0qnW!_zVIgNEt+Uolg5 zxy$kdbZ*q0cs(h{JE^LXZ=SNu_Tr}wQ{NgDz3IR0Ixnq|;`QGZ48~HB<%g-{gUcFqRdeRS1rr${s=c_ynuL;P_{&d6U``V4Z3fmuv z`~7zH-}#|?^S1lvzjxo(Gn$%q@vXCHUvW$4jgv7t4{pr07k~artoUPg%f^eZV`{|N zmgPhp{2Q`BL-=`x-JG>wuAPlo!#T;K;LyTD6IGt62YO7~G_&8xj5FzsN})#QVYB&V z8{K?aG$XxFO`jwo@yD&^{m(7VLbrt~RgX2AbYGc&$>$8`i{0k&90vl`3$7aK2>o7{ zB|I^+b@G*Ow|G-C`qneo=&^3({r`K%#Z7{LkIiGND=Xf^R=1FIUf$=lIq&Rt8fo$$ zo?x$G&+3hVQX6%#aGXC9cp<^|u+i`R3vsJXQk zE;*)e_1Ht`_@rCR)sZ!6_s(;LC&@%6g)2wD(&##I;1%~(N*_YMK=i1lpd~H&4!iE-FXg&Cb2pZ3@q@~H-$Jn8g4Yps$@=SKC3y32WVjEcm+Rxb`c z_Uy;9t6D30*{fcb8y-n8*7SEeXdq@W`JeCwht>`1AMRW~6RP2Ji{!Rj{#hureZU{Zxmwk?R%CVW>=g+_4e*W*1n;svw zF3H~5ugd3h^IPIVlk=PU@A7@lU3>GY8fWdhM{d98r!z&*`f>HTOjgd;FO{EF7)~8v z`(szJ@3rAH%Ztv3T$uLnvJ{?vWBGp9|9-o9Z$0^Je)4nery4nS9-sWs!oB_RcdpKo z^*m$ufT6ll?_P0RKycKqLoq%zcQ=PsUY~Jh;f`-ChqI>(h@b29oZahid)>_LY9Y%% zPqwk8+U>2GRiM#n>$&IJkps)UvqBPcR1d#CwY+oVVpIFf*2^9XB3b@UIl=kD+M{=) zcap-yL!mNWNjG*)2z{inO}28qs73!HhWvfkYnR`fqR^RZpR6{qw$av8M&azX+0nDi zyID#~?r@*|x$*a`Y3piVaH}qq*)=OX@6{sn7#l5RaqWc9p_!Ytwzxl4PqMjs;-Tt#ektbuNgsSAJ}6Xi zWv^@L+CD8SW0Fi@OYPcE{(5IgG6muX>n0r@tt_ zVRC+Y;*q@}56*nQ^?vSI?a2lD6>J-KHpl;x`*C2Qg#T^VM;p&+-dgQ?j?1d;S)_Mg z^|PJwQwy~|>=k{G^Ug^3OrToogehAUDn6@tZr>ICKDI;R_aq6S4_P6t8}qKt^gNWO z|L(>V{YMS$i3biW375U2tM}%oWaEj6675NECPrk2-g@nyc3_(XbMJvUxxH^odD72M zZshEr^xRWsqhR;Z-fMr)PE^aRTQkvCymD!=+NB%EH_uzWd*5phrSmEV!g_`EOE>sF z66d}0Z${zj+&eRKd6;EhuY3?!WYgD|s8w~%`RutJ%%v`!e}bQ@-;Vt~LoIjr6tz3X zPd8fb>zbGUYSPvafhXUD>d*dOUjC^&c;VOXmqB$O->ELSH>X?FdX}E4xV<-P{1HXz z!W+M1YQE3dvUAnPsk1-bGXHq&C$GXA?B$h(pgUD z=DwHPG2do>yCQ$^&E0Namt7H({CVWgZ&$mw6RzCx{@Z!x_?(7)%_Wlg*l{AJ1y}UQ+t3L#kuaj`uBRzeREFbydH^8p8Br$(rk| zcB)yv-FF>oUy2DI_i63)t9-cZ&+gSXrf+`#V(K@}G>PB2pMEZSEXrvy#f9}vcG8;0 zq{8NHcO(*cQ1Y znbPuOwOrzFqb~=qS2etPM4#mk>(^z+)oM~%?;l~zR?As3$^GhsGqRW8U+VVk@sdep zTtCOqLR2Gl_UuagtRC4;AC*n1jSItk&#o|DQ^0hM_uuv#bM90Kxb+Bk9KE+{^J+^i z@rp3HVx7)(y$>PUao)bV&u>3mwe|MpyrLKbcP`^=ymIVM3vCx{=-7R<@@mZM#};MB z-aY7uzuWTvf?8t9w)0s+Ho~)(-0JD}H$NJ-Iz;H^#uCl7MkYVjuYB-%#T|a-Yl{{f zxmTu9zLF)qFY(Kim6~~KUn*q1erGR}sK0(7yK{w74hxlXf6w^8w`lkz-M|96sq-fw(zI^aX3 zgrU#1Tkchx`-{UrmVlN@#&3PLGAt+4#{A;#o5y1#t?rgJ%2^5g;BHKRmt<>V^3YrR zXH?Hb&FqyAPS*59i!{G-=XWja*_(XaS91ILEB7Mepa75y1se;w4FcvDwzbcG%G@7HbwWr(8QAoQJuIiOCVao5%rF&Sr=KZP_aJ!OsuF+mez(VBlQkQ?rCVGnL zrUxay;dm3XsP~xCjwN@dJ2*)$IUzo|{YQvV`Q`6&p-lnV3K@TWMHVg8&;5GAVb#Ez317j64Hq$+}?iNXz7)2yXD=Edrz+33f^LqqSwjn`)5_-{NG#pbsN=QZZGaU z&*!SZYUaxENMq8cjBN+2zAk95ymY2}L8Ag|Va16BCUcfvX4|XY_3ge;;*^%N0iV7; z4EO0i936cAaaHr%i`;Qy?k(5!?XI8TpTx1fV%Ar+x2L8)Tg1gb>ErK^_{%Xh;wKZY z=oyut$}2qKqhz1`Ks`6vZFg38ijjP|-P=`CU$wK(ZR}4v*!**b($RY+uXfyfkg(xw zSmA*KEkYVT_B(@5S~Av7k5nnwKfq~tGwFJhf<^HUmQkjioMX=v4XLtnsHm~owD#ey`4v_D-LF5^Ecmo^tV>%ltqr- zp^EbdF3)s+yyNZrXZLTsT^_V^PTfZTkOwhW+TX@6cd6e0`{eV}22Kr5UEk-4Hyxgl z_;%O(Pq|BGo_YI2$K9iG%MQ`-4<{c^eh}>VDz!br>2jyJ$J@j|EDBYUhf7m;uU%$Q zY_a(o+x4(!kvY8PyY`oApTw8v(hG9;i?4j+xq8l?&7Qw~ zuis>!URf?U$@I5*@@sci$KdK*H?6AL1$lQ$b3a`CeaVlXHL5nLVbQaTa?g_l*-M^V z`SE>RHt)FC;+L;)-HPs<{rgF5)})Q+sRZ3I%32eZBnNj&EhjE|DTrRxwdV@5m#2{N=Yv zZz?%z*LCZx#-ol4_w;|&F-{jUo%H0%jlff~(|3l>pXzsa>g_w>GJ%E{u1r2Jm9X3; zJ?h8Fj51yBWX~vRorzJ;m)BhOT`1Nm)j3P7F6x$bc(Tv+%?cH*b>$@|cOQG@%KP^3 z{ZReCb>YWr%)jlDGh9;}|MKX$`b+z^Ty?%N+j_sUXHb4|;Ms-Grk5BLOx14IGY-48 zUr%iDl8f^^l^lgnCh40k?Pxw)^V)1`M!De8(i_LS@7-m{6gxh3GE)T`ODE5Td0zsA zrCpb1-{sajUVT5rd9%c&n5Wmbl&!tGR+mS#@Wv72?xd~P-;3Kv?7n+XI<_E4z{XRh z^P83Uda=pJ%#Qhcdv5wB;!+SgjO&+DX0f3{^P3z}{E)o={=cVOw9&#c zb1sWx=A5LF@Vqq~4;S6qVCt%N?5mo9amN5#>mx(=_xcAui3@$H@b25iFoHN+M>RR>l+=-}K{--Y&C7g_P@(K7nrO;*n z)Q_{g3bz_B`}ZN_;QrI0()%n$QYDV@Jle5#+g<wN-{rxU3n0Bw=hxxG&?cH0xZP~!Y z!E@+EkDw6`Z2 zj^B}Vjog}FfBaABZMD`)x36l=8n#(emkKTrbzCj2FH$;l=ZgTg!V8P%)`#C0{&;db z*Im7p!D}~iul-hZ?Z#!LX(ELM?_xq;Kl|}@>v|_+@y8Pq3x2rx7aLv-*--Ays(N|r znF+%2Om>xSiOouqiqeVQGt$=XV{f_lC5{O4<#OLj)mtCBE?Y0oHm+crhmgSJ?B0{RlwNl4jJRu`uBX-& z@#sxjlXNi6wb^BCH?p=;)Ss@TqLYMuB^?>hu?{xv=7FdDZk4rES|P*PXsq zc`Ni>7n7=z2w#Pk7o9{HQZFKNU>kQYYJ)3yh*03uJ-tu}>Zq}>bxu7(A!LP!TOzJBFbrO3OPG)UXW;1RTzjs6YX@t_}?2P1wP6_L%&;1Xz z=S|fw2-*9#_SLVf>!!6uDh9iiYm+#N?bG+)N#1Uev`F;YWfi#|p~Tloo{qeY$kkVw%ej2i{7iG>P^}H!hv!Kc`hWlHI(vH_rrq8D#3Ru= zn{W5FUkhffk?did;gJ&FJ6-R=@%2Aae@IQfx_&WRv1y#;jqs-vjuhVaeDSTD-*B7B zhsO!CuOIz3vv}i6;iDQBSJM8S+r_$W-tQE*M-D=_XH|rqS!rIsoU_z$>V?R%!cH~>3Lo9_RD4dwq&ao$@?x~+j<~-{*ADw4zsrH=&PIWnH0@zKfU_R zFWVNWa_@T|kJweEEy~&1)md^{uk3ZP1*5QM&Q8mE>Ag>;KU{C&)StOGZTmF^(L4Ov zHZ$(03$&R_2+dD_F8o{Q_RfgF+z3yOd`0It=H}+4J(64=^dvadyJ-g-YLeoi~RrGIV?T!C) za{0{I%>6s2AHJ!ye%%(iT@j1@EN9Jqy8Zk969?H(=f^INd&d%d`}D({t(M1Q|A;-X+?r|bW#GEUU?wEv_%C$!H0J~97U@Ur=KCl0pr@7un7ZqB!&^Hrw{r6c!P zruV+J-8o5S`;&_~GyC7mK0SC)JUVvqw~0Qwl_s-4ZTY?@>ty8a?(*O}Yk%sVxf_}G zL}T@m^`EqK`F~GY9W#Bm$LY)0if=^s8KurFEZ_V$!rn4&t0w=Z6F1j9-QfRo^UvGs zgJV>GuKH?|r`vz)#OiMKTN2ULJZ&rNzd0lU2(SPhMV8^zfDJ}IZAw`FET|IvAir`co%pNy!vbF4~d z+pdi}d`@q^R(vIj?{=E)j;Dp2JU)5Gr{1zYcmA33<{bM;>*9G{{oJ(GCii5xxMEq4 z^;~(mRIk^k_v8fcd!hV2$gpy5>da~Cxla;r7pd&t5??bTRc!NTn^(&AyAMS@-FBt# zX!!~AV~;%lrPsw6iRBmd&A(JMXBvC$sqSmrs{hm-h}d&O-!4lxe%r=-`gtGA)_(6+ zmjAAK`tRgFDLvlusRsMf_uu%uNOk_VZrhyd*MI+S@t^s2?vwAICu^?>j9#{;`B8fA zlho?d`S<^Px8JJy-_rVY^S&KPyDd+@R%x$2HF^8Kin7a}lg=C2y4Rixy#4K0{a^OI zS<3vsjnZ{1@3`B^T#nv-R_nF+Df#X57H_-9->3V%BXrXFTT}l2%=~9zeS1yd_WgYp zexJ6SFS;_xJ@?crJ?ZDMXA=JZ`2OD{FMZXWntwiSd_`X^Dz@!>lsmP%pJB)C#h1;t z*G`*OIccxq`$u8Bz#zi&}>_4i)B{>>_%x14>n z;F#9yqhE~d?|r^=bXMNq3RU*&61%q>vd5R}=xyfPclyxnEBdx^)z>A$pXVP7Jz9}J zJNv-;I-Y#9^!i%+RsFw9zie^*CzW(_4CqxUVYU3=hKt* zpUl*vS6b`}+!{d(c-v*Te^aeovaKe76KboSZ)*QT{^cYd$>ee|~E z`>)9#=W?oUHxbLPk`bT%*rF^$=iRTmkE`Bq-5s%7Cg$~wEjni3Yhym{i`jlRru=@v zpm{~{P^h3a*=bfQ?sv}&c3>7-nQ4pS=rmaOI}dExuZ}O6_kJumft)KLx{(JS_ia52(n^F66PVO-G7YjaX zoUmo?uAqc1U$xJ5zdBzmJ$-$blxwle?K8rEXB}6uzqQ(2&+hW88es;%{M0}HD&jNu z{_xxXdCh$7<>C7#{VsdW+t=n_ytQf9HN!nG=ghrS ze9dE1#&!et{}X=icmDkNXrX?k_^~?wb77Ts%XDs^`10)aBfWiXC3l`q-0?7C&(Y^6 zMV}qdpR+m7hW-DI<4Irt?5JBO6JKo`Qhw)s+MGMbc5QpM?|#kxirbH4>+bb#+)@4f zyLA82?!QS7|5Ql-K014C;QH&FdDrsRmD}gl#Od78|My{D*!F(i*u{0Pe?I>^yZQO{ zU)%R;^MCna&z$f)@A~g=F7xiy$miVO(^Ro<#-x|FKg%wbzueI;D-*uGk|&q#%)zI# z-z4stepfvC`?>z#Nq^^xR_E_)uQL+6zufXitW@{=*HtI#1d}gD+-XfN(yl+Q`&PQ_ zE{9lh68;93m6#~7#K7Z9hgCMfKvk-h+fdbzzd=s zI2oltbb}C+35aG;Wyt{3UK|&|^c1QYVEA2NLw8hF)TDWlY05XF?|OTm&WQg%&#r3H z<(@6(|5a}OQDOM7Z@p?-^0c2*-mHI@eo8BIw&@{>%$GB7c`mzl`S>q&28M>W!V7L* z^nL6hugm_N{bxa;{mIBl6Z1u0_*h!}S786CaQ>a_a@Bk#6NV3UuOjzL*h<@e7q6f9 zGeRXYZQ8t^_2E({PyBs)-e`7J)w~Et1_lQ~##wWV=S`e!_Nn5Z)!*gyCv3X67}lPC zFC(cddGoaA`=6`yi>GN`zP{U=;el=Oq>0yaryl$Z}*?`{~otAb?P@+>w{nG-&$tt_TN4s zUVG})F+qlV*Hc~Owokl&PQ3Es`Qm&Z&#?QIrJp^FE*0NOtGp0i`7Dryp<%7?f}1;g zAHP^ z39omxH_T08=3ux}qgd!|&&jtSx4xb!H*x8k%RNk*AAe$e^i21~5E`4TJyO XH4ofb9E~P^09Bx#u6{1-oD!M<_$;px From 8dc23d26215b28d61a2c708cf93034ec03a603ec Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 15:27:18 +0000 Subject: [PATCH 0187/1513] Hide the additional information button after use --- rmo/template/mock/nuisance.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rmo/template/mock/nuisance.html b/rmo/template/mock/nuisance.html index 8820cd92..aa2f88b6 100644 --- a/rmo/template/mock/nuisance.html +++ b/rmo/template/mock/nuisance.html @@ -35,6 +35,7 @@ function toggleCollapse(something) { } else { el.classList.add('collapse'); } + document.getElementById("toggle-additional").classList.add("visually-hidden"); } // Check for source identification document.addEventListener('DOMContentLoaded', function() { @@ -351,7 +352,7 @@ select.tall {

-
From 2baad02a0c7c8f47b8fc5dca93a51a72d4a54986 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 15:32:50 +0000 Subject: [PATCH 0188/1513] Make "submit another report" link work, remove "back home" button --- rmo/template/mock/nuisance-submit-complete.html | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/rmo/template/mock/nuisance-submit-complete.html b/rmo/template/mock/nuisance-submit-complete.html index 7af06e85..4249a70a 100644 --- a/rmo/template/mock/nuisance-submit-complete.html +++ b/rmo/template/mock/nuisance-submit-complete.html @@ -116,13 +116,7 @@ -
- {{ if not (eq .District nil) }} -

Your report will be handled by

-

{{ .District.Name }}

- - {{ end }} +
+
+ {{ if not (eq .District nil) }} +

Your report will be handled by

+

{{ .District.Name }}

+ + {{ end }} +
From c820ec91c6a2f2226a3b1bb4fa54b2d2136671d8 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 15:45:56 +0000 Subject: [PATCH 0190/1513] Add district name to periodic updates subscription --- rmo/template/mock/nuisance-submit-complete.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rmo/template/mock/nuisance-submit-complete.html b/rmo/template/mock/nuisance-submit-complete.html index 87b366f0..f5d11bb1 100644 --- a/rmo/template/mock/nuisance-submit-complete.html +++ b/rmo/template/mock/nuisance-submit-complete.html @@ -80,7 +80,7 @@
From 62ec73c6734b452b55186ca10d46e3ca88f08def Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 15:48:47 +0000 Subject: [PATCH 0191/1513] Add primary color to header bar --- rmo/template/component/header.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rmo/template/component/header.html b/rmo/template/component/header.html index 453a2aa3..6679d739 100644 --- a/rmo/template/component/header.html +++ b/rmo/template/component/header.html @@ -1,5 +1,5 @@ {{define "header"}} -

Please provide the location of the potential mosquito production source. We may be able to extract this information from your photos if they contain location data.

+
+
+

You can select the location by address or by moving the marker on the map.

+
+
@@ -347,8 +337,8 @@ function displaySelectedCoordinates(lngLat) {
-
From e7230eb3c22a5d220c34cdb15068fb05e3f7f885 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 15:56:46 +0000 Subject: [PATCH 0193/1513] Remove redundant contact information section It's handled in the next page. --- rmo/template/mock/water.html | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/rmo/template/mock/water.html b/rmo/template/mock/water.html index 110708aa..a919edf4 100644 --- a/rmo/template/mock/water.html +++ b/rmo/template/mock/water.html @@ -459,31 +459,6 @@ function displaySelectedCoordinates(lngLat) {
- - -
Your Contact Information (for updates)
-
-
- - -
-
- - -
-
- - -
-
-
- - -
-
-
From 78ee0483d3ab38957e55937fdc3f3b3f9092c85b Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 16:10:17 +0000 Subject: [PATCH 0194/1513] Add "this is my property" checkbox --- rmo/template/mock/water.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rmo/template/mock/water.html b/rmo/template/mock/water.html index a919edf4..ff6199a1 100644 --- a/rmo/template/mock/water.html +++ b/rmo/template/mock/water.html @@ -454,10 +454,16 @@ function displaySelectedCoordinates(lngLat) {
-
+
+
+
+ + +
+
From a41fdac13be2b325727e66bc45e73856b91579fd Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 16:14:52 +0000 Subject: [PATCH 0195/1513] Add checkbox granting backyard access --- rmo/template/mock/water.html | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rmo/template/mock/water.html b/rmo/template/mock/water.html index ff6199a1..0d7cd460 100644 --- a/rmo/template/mock/water.html +++ b/rmo/template/mock/water.html @@ -440,29 +440,33 @@ function displaySelectedCoordinates(lngLat) {
-

Contact Information

+

Property Owner Information (if known)

- -
Property Owner Information (if known)
-
+
-
+
-
+
+
+
- +
+
+ + +
From 5970e9c5a4e24fed6e6ddb3a399091e65e52ad55 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 16:22:35 +0000 Subject: [PATCH 0196/1513] Add anonymity checkbox --- rmo/template/mock/water.html | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/rmo/template/mock/water.html b/rmo/template/mock/water.html index 0d7cd460..fb8ab1a5 100644 --- a/rmo/template/mock/water.html +++ b/rmo/template/mock/water.html @@ -166,6 +166,14 @@ function toggleCollapse(something) { document.getElementById("toggle-additional").classList.add("visually-hidden"); } document.addEventListener('DOMContentLoaded', function() { + // Initialize tooltips + var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) + tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { + return new bootstrap.Tooltip(tooltipTriggerEl, { + trigger: 'hover focus' // Works on hover (desktop) and tap (mobile) + }); + }); + // Elements const photoInput = document.getElementById('photos'); @@ -464,8 +472,17 @@ function displaySelectedCoordinates(lngLat) {
- - + + +
+
+ +
From bc27a195345fe5576828cd85aa4aac4c2afcd3d1 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 16:32:13 +0000 Subject: [PATCH 0197/1513] Switch to dart-sass Faster, and hopefully less error-prone --- default.nix | 2 +- flake.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index 7068db64..700a599f 100644 --- a/default.nix +++ b/default.nix @@ -11,7 +11,7 @@ pkgs.buildGoModule rec { # Needs to be updated after every modification of go.mod/go.sum vendorHash = "sha256-aaJnH258H1LkXvb22rR3Clg7fKzA/HSmBZUkh1E8jKI="; - nativeBuildInputs = [ pkgs.sass ]; + nativeBuildInputs = [ pkgs.dart-sass ]; preBuild = '' diff --git a/flake.nix b/flake.nix index 69f7af77..ed2819ba 100644 --- a/flake.nix +++ b/flake.nix @@ -21,11 +21,11 @@ buildInputs = [ pkgs.air pkgs.autoprefixer + pkgs.dart-sass pkgs.go pkgs.goose pkgs.gotools pkgs.lefthook - pkgs.sass pkgs.watchexec ]; }; From 87893363e5e23bb165f93344628c0864ce6cf5fb Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 16:34:58 +0000 Subject: [PATCH 0198/1513] Remove redundant request logging --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 91c8de6a..6211f76d 100644 --- a/main.go +++ b/main.go @@ -96,7 +96,7 @@ func main() { r.Use(LoggerMiddleware(&router_logger)) r.Use(middleware.RequestID) r.Use(middleware.RealIP) - r.Use(middleware.Logger) + //r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(sentryMiddleware.Handle) r.Use(auth.NewSessionManager().LoadAndSave) From 3bf40572e2f4343a7c91afe66c254f3bd803836c Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 18:00:56 +0000 Subject: [PATCH 0199/1513] Rename rmo/page.go to rmo/template.go Because it contains stuff for dealing with templates. --- rmo/{page.go => template.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rmo/{page.go => template.go} (100%) diff --git a/rmo/page.go b/rmo/template.go similarity index 100% rename from rmo/page.go rename to rmo/template.go From 2bd848fa97cde83e35a3f39ba81b01120a614ac3 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 18:01:39 +0000 Subject: [PATCH 0200/1513] Rename rmo/endpoint.go to rmo/root.go Because it contains the root page logic. --- rmo/{endpoint.go => root.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rmo/{endpoint.go => root.go} (100%) diff --git a/rmo/endpoint.go b/rmo/root.go similarity index 100% rename from rmo/endpoint.go rename to rmo/root.go From 9b1d75d47f16f7c0eb6c6ca667e4dd25cf3575e5 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 18:21:27 +0000 Subject: [PATCH 0201/1513] Rename htmlpage to html Because it's going to get more tools. --- .gitignore | 4 +-- README.md | 4 +-- default.nix | 2 +- {htmlpage => html}/fileserver.go | 27 +++++++++++------- {htmlpage => html}/h3.go | 2 +- {htmlpage => html}/html.go | 2 +- {htmlpage => html}/response.go | 2 +- {htmlpage => html}/static.go | 6 ++-- {htmlpage => html}/static/css/placeholder | 0 {htmlpage => html}/static/favicon-rmo.ico | Bin {htmlpage => html}/static/favicon-sync.ico | Bin .../static/img/nidus-logo-256-transparent.png | Bin .../static/img/nidus-logo-no-lettering-64.png | Bin .../static/img/rmo-logo-224.png | Bin {htmlpage => html}/static/img/rmo/banner.jpg | Bin .../static/js/address-display.js | 0 .../static/js/address-suggestion.js | 0 {htmlpage => html}/static/js/geocode.js | 0 {htmlpage => html}/static/js/location.js | 0 {htmlpage => html}/static/js/map-aggregate.js | 0 {htmlpage => html}/static/js/map-locator.js | 0 .../static/js/map-multipoint.js | 0 .../static/js/map-with-markers.js | 0 {htmlpage => html}/static/js/report-table.js | 0 .../static/vendor/css/bootstrap.min.css | 0 .../static/vendor/js/bootstrap.bundle.min.js | 0 .../static/vendor/js/bootstrap.min.js | 0 rmo/mock.go | 7 ++--- rmo/nuisance.go | 6 ++-- rmo/pool.go | 6 ++-- rmo/quick.go | 8 +++--- rmo/root.go | 6 ++-- rmo/routes.go | 4 +-- rmo/search.go | 4 +-- rmo/status.go | 14 ++++----- rmo/template.go | 6 ++-- sync/dash.go | 16 +++++------ sync/mock.go | 6 ++-- sync/oauth.go | 4 +-- sync/page.go | 6 ++-- sync/privacy.go | 4 +-- sync/routes.go | 4 +-- sync/signin.go | 6 ++-- sync/text.go | 4 +-- 44 files changed, 83 insertions(+), 77 deletions(-) rename {htmlpage => html}/fileserver.go (88%) rename {htmlpage => html}/h3.go (97%) rename {htmlpage => html}/html.go (99%) rename {htmlpage => html}/response.go (98%) rename {htmlpage => html}/static.go (75%) rename {htmlpage => html}/static/css/placeholder (100%) rename {htmlpage => html}/static/favicon-rmo.ico (100%) rename {htmlpage => html}/static/favicon-sync.ico (100%) rename {htmlpage => html}/static/img/nidus-logo-256-transparent.png (100%) rename {htmlpage => html}/static/img/nidus-logo-no-lettering-64.png (100%) rename {htmlpage => html}/static/img/rmo-logo-224.png (100%) rename {htmlpage => html}/static/img/rmo/banner.jpg (100%) rename {htmlpage => html}/static/js/address-display.js (100%) rename {htmlpage => html}/static/js/address-suggestion.js (100%) rename {htmlpage => html}/static/js/geocode.js (100%) rename {htmlpage => html}/static/js/location.js (100%) rename {htmlpage => html}/static/js/map-aggregate.js (100%) rename {htmlpage => html}/static/js/map-locator.js (100%) rename {htmlpage => html}/static/js/map-multipoint.js (100%) rename {htmlpage => html}/static/js/map-with-markers.js (100%) rename {htmlpage => html}/static/js/report-table.js (100%) rename {htmlpage => html}/static/vendor/css/bootstrap.min.css (100%) rename {htmlpage => html}/static/vendor/js/bootstrap.bundle.min.js (100%) rename {htmlpage => html}/static/vendor/js/bootstrap.min.js (100%) diff --git a/.gitignore b/.gitignore index 2c345b04..36b3665b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ nidus-sync -htmlpage/static/css/bootstrap.css -htmlpage/static/css/bootstrap.css.map +html/static/css/bootstrap.css +html/static/css/bootstrap.css.map .sass-cache/ tmp/ diff --git a/README.md b/README.md index 8cfe8ff5..521e07f1 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ We're using a customized Bootstrap theme for this site. You'll need to build the ``` nix develop cd scss -scss custom.scss > ../htmlpage/static/css/bootstrap.css +scss custom.scss > ../html/static/css/bootstrap.css ``` ## Running @@ -90,5 +90,5 @@ This uses [goose](https://github.com/pressly/goose). You can use the goose comma For iterating on styles ``` -watchexec -e *.scss sass --style=compressed scss/custom.scss:htmlpage/static/css/bootstrap.css +watchexec -e *.scss sass --style=compressed scss/custom.scss:html/static/css/bootstrap.css ``` diff --git a/default.nix b/default.nix index 700a599f..e38aec3f 100644 --- a/default.nix +++ b/default.nix @@ -16,7 +16,7 @@ pkgs.buildGoModule rec { preBuild = '' SASS_SRC_DIR="./scss" - CSS_OUTPUT_DIR="./htmlpage/static/css/" + CSS_OUTPUT_DIR="./html/static/css/" mkdir -p "$CSS_OUTPUT_DIR" diff --git a/htmlpage/fileserver.go b/html/fileserver.go similarity index 88% rename from htmlpage/fileserver.go rename to html/fileserver.go index 5dc69871..245b8d1c 100644 --- a/htmlpage/fileserver.go +++ b/html/fileserver.go @@ -1,4 +1,4 @@ -package htmlpage +package html import ( "embed" @@ -12,6 +12,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/go-chi/chi/v5" + "github.com/rs/zerolog/log" ) // FileServer conveniently sets up a http.FileServer handler to serve @@ -36,9 +37,21 @@ func FileServer(r chi.Router, path string, root http.FileSystem, embeddedFS embe var err error var fileToServe http.File + found := false - if config.IsProductionEnvironment() { - // For production use the embedded filesystem + // For dev, try the current filesystem + if !config.IsProductionEnvironment() { + // Try to open from local filesystem for development + fileToServe, err = root.Open(requestedPath) + if err != nil { + log.Warn().Str("path", requestedPath).Msg("Failed to read static file for dev") + found = false + } else { + found = true + } + } + // For production use the embedded filesystem + if !found { embeddedFilePath := filepath.Join(embeddedPath, requestedPath) embeddedFile, err := embeddedFS.Open(embeddedFilePath) @@ -49,14 +62,6 @@ func FileServer(r chi.Router, path string, root http.FileSystem, embeddedFS embe // Wrap the embedded file to implement http.File interface fileToServe = &embeddedFileWrapper{embeddedFile} - - } else { - // Try to open from local filesystem for development - fileToServe, err = root.Open(requestedPath) - if err != nil { - RespondError(w, "Failed to open file", err, http.StatusNotFound) - return - } } // Create a custom ResponseWriter that allows us to modify headers diff --git a/htmlpage/h3.go b/html/h3.go similarity index 97% rename from htmlpage/h3.go rename to html/h3.go index 6de2b817..644db8c4 100644 --- a/htmlpage/h3.go +++ b/html/h3.go @@ -1,4 +1,4 @@ -package htmlpage +package html import ( "fmt" diff --git a/htmlpage/html.go b/html/html.go similarity index 99% rename from htmlpage/html.go rename to html/html.go index a3882f55..f16312d8 100644 --- a/htmlpage/html.go +++ b/html/html.go @@ -1,4 +1,4 @@ -package htmlpage +package html import ( "bytes" diff --git a/htmlpage/response.go b/html/response.go similarity index 98% rename from htmlpage/response.go rename to html/response.go index 74b5cc2d..33e1d386 100644 --- a/htmlpage/response.go +++ b/html/response.go @@ -1,4 +1,4 @@ -package htmlpage +package html import ( "net/http" diff --git a/htmlpage/static.go b/html/static.go similarity index 75% rename from htmlpage/static.go rename to html/static.go index 0a544c00..c6fbb429 100644 --- a/htmlpage/static.go +++ b/html/static.go @@ -1,10 +1,12 @@ -package htmlpage +package html import ( "embed" + "io/fs" "net/http" "github.com/go-chi/chi/v5" + "github.com/rs/zerolog/log" ) //go:embed static/* @@ -14,7 +16,7 @@ var localFS http.Dir func AddStaticRoute(r chi.Router, path string) { if localFS == "" { - localFS = http.Dir("./htmlpage/static") + localFS = http.Dir("./html/static") } FileServer(r, "/static", localFS, EmbeddedStaticFS, "static") } diff --git a/htmlpage/static/css/placeholder b/html/static/css/placeholder similarity index 100% rename from htmlpage/static/css/placeholder rename to html/static/css/placeholder diff --git a/htmlpage/static/favicon-rmo.ico b/html/static/favicon-rmo.ico similarity index 100% rename from htmlpage/static/favicon-rmo.ico rename to html/static/favicon-rmo.ico diff --git a/htmlpage/static/favicon-sync.ico b/html/static/favicon-sync.ico similarity index 100% rename from htmlpage/static/favicon-sync.ico rename to html/static/favicon-sync.ico diff --git a/htmlpage/static/img/nidus-logo-256-transparent.png b/html/static/img/nidus-logo-256-transparent.png similarity index 100% rename from htmlpage/static/img/nidus-logo-256-transparent.png rename to html/static/img/nidus-logo-256-transparent.png diff --git a/htmlpage/static/img/nidus-logo-no-lettering-64.png b/html/static/img/nidus-logo-no-lettering-64.png similarity index 100% rename from htmlpage/static/img/nidus-logo-no-lettering-64.png rename to html/static/img/nidus-logo-no-lettering-64.png diff --git a/htmlpage/static/img/rmo-logo-224.png b/html/static/img/rmo-logo-224.png similarity index 100% rename from htmlpage/static/img/rmo-logo-224.png rename to html/static/img/rmo-logo-224.png diff --git a/htmlpage/static/img/rmo/banner.jpg b/html/static/img/rmo/banner.jpg similarity index 100% rename from htmlpage/static/img/rmo/banner.jpg rename to html/static/img/rmo/banner.jpg diff --git a/htmlpage/static/js/address-display.js b/html/static/js/address-display.js similarity index 100% rename from htmlpage/static/js/address-display.js rename to html/static/js/address-display.js diff --git a/htmlpage/static/js/address-suggestion.js b/html/static/js/address-suggestion.js similarity index 100% rename from htmlpage/static/js/address-suggestion.js rename to html/static/js/address-suggestion.js diff --git a/htmlpage/static/js/geocode.js b/html/static/js/geocode.js similarity index 100% rename from htmlpage/static/js/geocode.js rename to html/static/js/geocode.js diff --git a/htmlpage/static/js/location.js b/html/static/js/location.js similarity index 100% rename from htmlpage/static/js/location.js rename to html/static/js/location.js diff --git a/htmlpage/static/js/map-aggregate.js b/html/static/js/map-aggregate.js similarity index 100% rename from htmlpage/static/js/map-aggregate.js rename to html/static/js/map-aggregate.js diff --git a/htmlpage/static/js/map-locator.js b/html/static/js/map-locator.js similarity index 100% rename from htmlpage/static/js/map-locator.js rename to html/static/js/map-locator.js diff --git a/htmlpage/static/js/map-multipoint.js b/html/static/js/map-multipoint.js similarity index 100% rename from htmlpage/static/js/map-multipoint.js rename to html/static/js/map-multipoint.js diff --git a/htmlpage/static/js/map-with-markers.js b/html/static/js/map-with-markers.js similarity index 100% rename from htmlpage/static/js/map-with-markers.js rename to html/static/js/map-with-markers.js diff --git a/htmlpage/static/js/report-table.js b/html/static/js/report-table.js similarity index 100% rename from htmlpage/static/js/report-table.js rename to html/static/js/report-table.js diff --git a/htmlpage/static/vendor/css/bootstrap.min.css b/html/static/vendor/css/bootstrap.min.css similarity index 100% rename from htmlpage/static/vendor/css/bootstrap.min.css rename to html/static/vendor/css/bootstrap.min.css diff --git a/htmlpage/static/vendor/js/bootstrap.bundle.min.js b/html/static/vendor/js/bootstrap.bundle.min.js similarity index 100% rename from htmlpage/static/vendor/js/bootstrap.bundle.min.js rename to html/static/vendor/js/bootstrap.bundle.min.js diff --git a/htmlpage/static/vendor/js/bootstrap.min.js b/html/static/vendor/js/bootstrap.min.js similarity index 100% rename from htmlpage/static/vendor/js/bootstrap.min.js rename to html/static/vendor/js/bootstrap.min.js diff --git a/rmo/mock.go b/rmo/mock.go index 3255eab9..a49554bd 100644 --- a/rmo/mock.go +++ b/rmo/mock.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/go-chi/chi/v5" ) @@ -57,17 +57,16 @@ func makeContentURL(slug string) ContentURL { Water: makeURLMock(slug, "water"), } } - func makeURLMock(slug, p string) string { return config.MakeURLReport("/mock/district/%s/%s", slug, p) } -func renderMock(t *htmlpage.BuiltTemplate) func(http.ResponseWriter, *http.Request) { +func renderMock(t *html.BuiltTemplate) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { slug := chi.URLParam(r, "slug") if slug == "" { slug = "delta-mvcd" } - htmlpage.RenderOrError( + html.RenderOrError( w, t, ContentMock{ diff --git a/rmo/nuisance.go b/rmo/nuisance.go index ace0ee08..5580daa4 100644 --- a/rmo/nuisance.go +++ b/rmo/nuisance.go @@ -9,7 +9,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" "github.com/rs/zerolog/log" @@ -26,7 +26,7 @@ var ( ) func getNuisance(w http.ResponseWriter, r *http.Request) { - htmlpage.RenderOrError( + html.RenderOrError( w, Nuisance, ContextNuisance{}, @@ -34,7 +34,7 @@ func getNuisance(w http.ResponseWriter, r *http.Request) { } func getNuisanceSubmitComplete(w http.ResponseWriter, r *http.Request) { report := r.URL.Query().Get("report") - htmlpage.RenderOrError( + html.RenderOrError( w, NuisanceSubmitComplete, ContextNuisanceSubmitComplete{ diff --git a/rmo/pool.go b/rmo/pool.go index 5b6adb08..6abb7efe 100644 --- a/rmo/pool.go +++ b/rmo/pool.go @@ -13,7 +13,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/aarondl/opt/omit" "github.com/rs/zerolog/log" ) @@ -31,7 +31,7 @@ var ( ) func getPool(w http.ResponseWriter, r *http.Request) { - htmlpage.RenderOrError( + html.RenderOrError( w, Pool, ContextPool{ @@ -41,7 +41,7 @@ func getPool(w http.ResponseWriter, r *http.Request) { } func getPoolSubmitComplete(w http.ResponseWriter, r *http.Request) { report := r.URL.Query().Get("report") - htmlpage.RenderOrError( + html.RenderOrError( w, PoolSubmitComplete, ContextPoolSubmitComplete{ diff --git a/rmo/quick.go b/rmo/quick.go index 87606e71..d001d358 100644 --- a/rmo/quick.go +++ b/rmo/quick.go @@ -16,7 +16,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/h3utils" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/Gleipnir-Technology/nidus-sync/platform/text" "github.com/aarondl/opt/omit" @@ -44,7 +44,7 @@ var ( ) func getQuick(w http.ResponseWriter, r *http.Request) { - htmlpage.RenderOrError( + html.RenderOrError( w, quickT, ContentQuick{}, @@ -80,7 +80,7 @@ func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) { } } } - htmlpage.RenderOrError( + html.RenderOrError( w, quickSubmitCompleteT, ContentQuickSubmitComplete{ @@ -91,7 +91,7 @@ func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) { } func getRegisterNotificationsComplete(w http.ResponseWriter, r *http.Request) { report := r.URL.Query().Get("report") - htmlpage.RenderOrError( + html.RenderOrError( w, registerNotificationsCompleteT, ContentRegisterNotificationsComplete{ diff --git a/rmo/root.go b/rmo/root.go index a2cf985e..18b5284f 100644 --- a/rmo/root.go +++ b/rmo/root.go @@ -5,7 +5,7 @@ import ( "net/http" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/rs/zerolog/log" ) @@ -24,7 +24,7 @@ var ( ) func getPrivacy(w http.ResponseWriter, r *http.Request) { - htmlpage.RenderOrError( + html.RenderOrError( w, PrivacyT, ContentPrivacy{ @@ -36,7 +36,7 @@ func getPrivacy(w http.ResponseWriter, r *http.Request) { ) } func getRoot(w http.ResponseWriter, r *http.Request) { - htmlpage.RenderOrError( + html.RenderOrError( w, RootT, ContentRoot{}, diff --git a/rmo/routes.go b/rmo/routes.go index 105f2422..415fb836 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -1,7 +1,7 @@ package rmo import ( - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/go-chi/chi/v5" ) @@ -28,6 +28,6 @@ func Router() chi.Router { r.Get("/status", getStatus) r.Get("/status/{report_id}", getStatusByID) r.Get("/terms-of-service", getTerms) - htmlpage.AddStaticRoute(r, "/static") + html.AddStaticRoute(r, "/static") return r } diff --git a/rmo/search.go b/rmo/search.go index d5f0d3ac..d37dbd3a 100644 --- a/rmo/search.go +++ b/rmo/search.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" ) type ContentSearch struct { @@ -17,7 +17,7 @@ var ( ) func getSearch(w http.ResponseWriter, r *http.Request) { - htmlpage.RenderOrError( + html.RenderOrError( w, Search, ContentSearch{ diff --git a/rmo/status.go b/rmo/status.go index 98b134ea..402937c1 100644 --- a/rmo/status.go +++ b/rmo/status.go @@ -14,7 +14,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/db/sql" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" "github.com/stephenafamo/scan" @@ -84,7 +84,7 @@ func formatReportID(s string) string { func getStatus(w http.ResponseWriter, r *http.Request) { report_id_str := r.URL.Query().Get("report") if report_id_str == "" { - htmlpage.RenderOrError( + html.RenderOrError( w, Status, ContentStatus{ @@ -103,7 +103,7 @@ func getStatus(w http.ResponseWriter, r *http.Request) { } if len(results) != 1 { log.Error().Int("count", len(results)).Str("report_id", report_id_str).Msg("Got too many results for report id. This is a programmer error.") - htmlpage.RenderOrError( + html.RenderOrError( w, Status, ContentStatus{ @@ -117,7 +117,7 @@ func getStatus(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, fmt.Sprintf("/status/%s", report_id), http.StatusFound) return } - htmlpage.RenderOrError( + html.RenderOrError( w, Status, ContentStatus{ @@ -226,7 +226,7 @@ func getStatusByID(w http.ResponseWriter, r *http.Request) { content, err = contentFromQuick(ctx, report_id) } content.MapboxToken = config.MapboxToken - htmlpage.RenderOrError( + html.RenderOrError( w, StatusByID, content, @@ -235,7 +235,7 @@ func getStatusByID(w http.ResponseWriter, r *http.Request) { /* func getQuick(w http.ResponseWriter, r *http.Request) { - htmlpage.RenderOrError( + html.RenderOrError( w, Quick, ContentQuick{}, @@ -244,7 +244,7 @@ func getStatusByID(w http.ResponseWriter, r *http.Request) { func getQuickSubmitComplete(w http.ResponseWriter, r *http.Request) { report := r.URL.Query().Get("report") - htmlpage.RenderOrError( + html.RenderOrError( w, QuickSubmitComplete, ContentQuickSubmitComplete{ diff --git a/rmo/template.go b/rmo/template.go index c7f6ebc4..f7db337d 100644 --- a/rmo/template.go +++ b/rmo/template.go @@ -4,7 +4,7 @@ import ( "embed" "fmt" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" ) //go:embed template/* @@ -13,7 +13,7 @@ var embeddedFiles embed.FS var components = [...]string{"footer", "header", "photo-upload", "photo-upload-header"} var svgs = [...]string{"check-report", "mosquito", "pond"} -func buildTemplate(files ...string) *htmlpage.BuiltTemplate { +func buildTemplate(files ...string) *html.BuiltTemplate { subdir := "rmo" full_files := make([]string, 0) for _, f := range files { @@ -25,5 +25,5 @@ func buildTemplate(files ...string) *htmlpage.BuiltTemplate { for _, c := range svgs { full_files = append(full_files, fmt.Sprintf("%s/template/svg/%s.svg", subdir, c)) } - return htmlpage.NewBuiltTemplate(embeddedFiles, "rmo/", full_files...) + return html.NewBuiltTemplate(embeddedFiles, "rmo/", full_files...) } diff --git a/sync/dash.go b/sync/dash.go index e0b58016..22ef7987 100644 --- a/sync/dash.go +++ b/sync/dash.go @@ -14,7 +14,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/h3utils" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/uber/h3-go/v4" @@ -97,7 +97,7 @@ func getDistrict(w http.ResponseWriter, r *http.Request) { context := ContextDistrict{ MapboxToken: config.MapboxToken, } - htmlpage.RenderOrError(w, districtT, &context) + html.RenderOrError(w, districtT, &context) } func getLayoutTest(w http.ResponseWriter, r *http.Request, u *models.User) { @@ -106,7 +106,7 @@ func getLayoutTest(w http.ResponseWriter, r *http.Request, u *models.User) { respondError(w, "Failed to get user", err, http.StatusInternalServerError) return } - htmlpage.RenderOrError(w, layoutTestT, &ContentLayoutTest{User: userContent}) + html.RenderOrError(w, layoutTestT, &ContentLayoutTest{User: userContent}) } func getRoot(w http.ResponseWriter, r *http.Request) { @@ -241,7 +241,7 @@ func cell(ctx context.Context, w http.ResponseWriter, user *models.User, c int64 Treatments: treatments, User: userContent, } - htmlpage.RenderOrError(w, cellT, &data) + html.RenderOrError(w, cellT, &data) } func dashboard(ctx context.Context, w http.ResponseWriter, user *models.User) { @@ -310,7 +310,7 @@ func dashboard(ctx context.Context, w http.ResponseWriter, user *models.User) { RecentRequests: requests, User: userContent, } - htmlpage.RenderOrError(w, dashboardT, data) + html.RenderOrError(w, dashboardT, data) } func settings(w http.ResponseWriter, r *http.Request, user *models.User) { @@ -322,7 +322,7 @@ func settings(w http.ResponseWriter, r *http.Request, user *models.User) { data := ContentAuthenticatedPlaceholder{ User: userContent, } - htmlpage.RenderOrError(w, settingsT, data) + html.RenderOrError(w, settingsT, data) } func source(w http.ResponseWriter, r *http.Request, user *models.User, id uuid.UUID) { @@ -383,7 +383,7 @@ func source(w http.ResponseWriter, r *http.Request, user *models.User, id uuid.U User: userContent, } - htmlpage.RenderOrError(w, sourceT, data) + html.RenderOrError(w, sourceT, data) } func trap(w http.ResponseWriter, r *http.Request, user *models.User, id uuid.UUID) { @@ -423,5 +423,5 @@ func trap(w http.ResponseWriter, r *http.Request, user *models.User, id uuid.UUI User: userContent, } - htmlpage.RenderOrError(w, trapT, data) + html.RenderOrError(w, trapT, data) } diff --git a/sync/mock.go b/sync/mock.go index 923c3840..e61dd847 100644 --- a/sync/mock.go +++ b/sync/mock.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" "github.com/skip2/go-qrcode" @@ -125,13 +125,13 @@ func mock(t string, w http.ResponseWriter, code string) { SettingUserAdd: "/mock/setting/user/add", }, } - template, ok := htmlpage.TemplatesByFilename[t+".html"] + template, ok := html.TemplatesByFilename[t+".html"] if !ok { log.Error().Str("template", t).Msg("Failed to find template") respondError(w, "Failed to render template", nil, http.StatusInternalServerError) return } - htmlpage.RenderOrError(w, &template, data) + html.RenderOrError(w, &template, data) } func renderMock(templateName string) http.HandlerFunc { diff --git a/sync/oauth.go b/sync/oauth.go index 25129363..7dc9d443 100644 --- a/sync/oauth.go +++ b/sync/oauth.go @@ -9,7 +9,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/background" "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/rs/zerolog/log" ) @@ -89,5 +89,5 @@ func oauthPrompt(w http.ResponseWriter, r *http.Request, user *models.User) { data := ContextOauthPrompt{ User: userContent, } - htmlpage.RenderOrError(w, oauthPromptT, data) + html.RenderOrError(w, oauthPromptT, data) } diff --git a/sync/page.go b/sync/page.go index 291c5e05..50d812ee 100644 --- a/sync/page.go +++ b/sync/page.go @@ -5,7 +5,7 @@ import ( "fmt" "net/http" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/rs/zerolog/log" ) @@ -14,7 +14,7 @@ var embeddedFiles embed.FS var components = [...]string{"header", "icons", "map", "sidebar"} -func buildTemplate(files ...string) *htmlpage.BuiltTemplate { +func buildTemplate(files ...string) *html.BuiltTemplate { subdir := "sync" full_files := make([]string, 0) for _, f := range files { @@ -23,7 +23,7 @@ func buildTemplate(files ...string) *htmlpage.BuiltTemplate { for _, c := range components { full_files = append(full_files, fmt.Sprintf("%s/template/components/%s.html", subdir, c)) } - return htmlpage.NewBuiltTemplate(embeddedFiles, "sync/", full_files...) + return html.NewBuiltTemplate(embeddedFiles, "sync/", full_files...) } // Respond with an error that is visible to the user diff --git a/sync/privacy.go b/sync/privacy.go index 9631a1aa..a1e98279 100644 --- a/sync/privacy.go +++ b/sync/privacy.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" ) type ContentPrivacy struct { @@ -19,7 +19,7 @@ var ( ) func getPrivacy(w http.ResponseWriter, r *http.Request) { - htmlpage.RenderOrError( + html.RenderOrError( w, PrivacyT, ContentPrivacy{ diff --git a/sync/routes.go b/sync/routes.go index abb2be6b..86c14bb9 100644 --- a/sync/routes.go +++ b/sync/routes.go @@ -3,7 +3,7 @@ package sync import ( "github.com/Gleipnir-Technology/nidus-sync/api" "github.com/Gleipnir-Technology/nidus-sync/auth" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/go-chi/chi/v5" ) @@ -67,6 +67,6 @@ func Router() chi.Router { r.Method("GET", "/trap/{globalid}", auth.NewEnsureAuth(getTrap)) r.Method("GET", "/text/{destination}", auth.NewEnsureAuth(getTextMessages)) - htmlpage.AddStaticRoute(r, "/static") + html.AddStaticRoute(r, "/static") return r } diff --git a/sync/signin.go b/sync/signin.go index 6b72d011..40c91861 100644 --- a/sync/signin.go +++ b/sync/signin.go @@ -7,7 +7,7 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/auth" "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/rs/zerolog/log" ) @@ -92,10 +92,10 @@ func signin(w http.ResponseWriter, errorCode string) { data := ContentSignin{ InvalidCredentials: errorCode == "invalid-credentials", } - htmlpage.RenderOrError(w, signinT, data) + html.RenderOrError(w, signinT, data) } func signup(w http.ResponseWriter, path string) { data := ContentSignup{} - htmlpage.RenderOrError(w, signupT, data) + html.RenderOrError(w, signupT, data) } diff --git a/sync/text.go b/sync/text.go index 2d91cbea..473b51e7 100644 --- a/sync/text.go +++ b/sync/text.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/Gleipnir-Technology/nidus-sync/db/models" - "github.com/Gleipnir-Technology/nidus-sync/htmlpage" + "github.com/Gleipnir-Technology/nidus-sync/html" ) type ContentTextMessages struct { @@ -24,5 +24,5 @@ func getTextMessages(w http.ResponseWriter, r *http.Request, u *models.User) { content := ContentTextMessages{ User: userContent, } - htmlpage.RenderOrError(w, textMessagesT, content) + html.RenderOrError(w, textMessagesT, content) } From bb9dd1754f6fef8ed9f7558d53cd035e5ee83968 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 18:22:16 +0000 Subject: [PATCH 0202/1513] Start setting up structure for generating URLs This is to eventually avoid adding URLs through hard-coded strings to our templates. --- rmo/mock.go | 11 ++--------- rmo/root.go | 48 +++++++++++++++++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/rmo/mock.go b/rmo/mock.go index a49554bd..7d6a6599 100644 --- a/rmo/mock.go +++ b/rmo/mock.go @@ -22,13 +22,6 @@ type ContentDistrict struct { URLLogo string URLWebsite string } -type ContentURL struct { - Nuisance string - NuisanceSubmitComplete string - Status string - Tegola string - Water string -} type ContentMock struct { District ContentDistrict MapboxToken string @@ -48,7 +41,7 @@ func addMockRoutes(r chi.Router) { r.Get("/status", renderMock(mockStatusT)) } -func makeContentURL(slug string) ContentURL { +func makeContentURLMock(slug string) ContentURL { return ContentURL{ Nuisance: makeURLMock(slug, "nuisance"), NuisanceSubmitComplete: makeURLMock(slug, "nuisance-submit-complete"), @@ -77,7 +70,7 @@ func renderMock(t *html.BuiltTemplate) func(http.ResponseWriter, *http.Request) }, MapboxToken: config.MapboxToken, ReportID: "abcd-1234-5678", - URL: makeContentURL(slug), + URL: makeContentURLMock(slug), }, ) } diff --git a/rmo/root.go b/rmo/root.go index 18b5284f..b9edf4ab 100644 --- a/rmo/root.go +++ b/rmo/root.go @@ -15,7 +15,16 @@ type ContentPrivacy struct { Site string URLReport string } -type ContentRoot struct{} +type ContentRoot struct { + URL ContentURL +} +type ContentURL struct { + Nuisance string + NuisanceSubmitComplete string + Status string + Tegola string + Water string +} var ( PrivacyT = buildTemplate("privacy", "base") @@ -23,6 +32,14 @@ var ( TermsT = buildTemplate("terms", "base") ) +func boolFromForm(r *http.Request, k string) bool { + s := r.PostFormValue(k) + if s == "on" { + return true + } + return false +} + func getPrivacy(w http.ResponseWriter, r *http.Request) { html.RenderOrError( w, @@ -48,27 +65,26 @@ func getRobots(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "Allow: /\n") } func getTerms(w http.ResponseWriter, r *http.Request) { - htmlpage.RenderOrError( + html.RenderOrError( w, TermsT, ContentRoot{}, ) } -// Respond with an error that is visible to the user -func respondError(w http.ResponseWriter, m string, e error, s int) { - log.Warn().Int("status", s).Err(e).Str("user message", m).Msg("Responding with an error") - http.Error(w, m, s) -} - -func boolFromForm(r *http.Request, k string) bool { - s := r.PostFormValue(k) - if s == "on" { - return true +func makeContentURL(slug string) ContentURL { + return ContentURL{ + Nuisance: makeURL("nuisance"), + NuisanceSubmitComplete: makeURL("nuisance-submit-complete"), + Status: makeURL("status"), + Tegola: config.MakeURLTegola("/"), + Water: makeURL("water"), } - return false } +func makeURL(p string) string { + return config.MakeURLReport("/%s", p) +} func postFormValueOrNone(r *http.Request, k string) string { v := r.PostFormValue(k) if v == "" { @@ -76,3 +92,9 @@ func postFormValueOrNone(r *http.Request, k string) string { } return v } + +// Respond with an error that is visible to the user +func respondError(w http.ResponseWriter, m string, e error, s int) { + log.Warn().Int("status", s).Err(e).Str("user message", m).Msg("Responding with an error") + http.Error(w, m, s) +} From 38b1cdbbad3c9de5dd9e067ea35ad238d3e7e60a Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 18:58:50 +0000 Subject: [PATCH 0203/1513] Auto transform SVGs into template portions This means I don't have to modify the files correctly by hand --- html/html.go | 55 +++++++++++++++++++++++++++---- rmo/template.go | 5 +-- rmo/template/root.html | 12 ++----- rmo/template/svg/check-report.svg | 2 -- rmo/template/svg/mosquito.svg | 2 -- rmo/template/svg/pond.svg | 2 -- sync/page.go | 2 +- 7 files changed, 55 insertions(+), 25 deletions(-) diff --git a/html/html.go b/html/html.go index f16312d8..c3b99fa6 100644 --- a/html/html.go +++ b/html/html.go @@ -26,6 +26,7 @@ var TemplatesByFilename = make(map[string]BuiltTemplate, 0) type BuiltTemplate struct { files []string subdir string + svgs []string // Nil if we are going to read templates off disk every time we render // because we are in development mode. template *template.Template @@ -34,7 +35,7 @@ type BuiltTemplate struct { func (bt *BuiltTemplate) executeTemplate(w io.Writer, data any) error { if bt.template == nil { name := path.Base(bt.files[0]) - templ, err := parseFromDisk(bt.files) + templ, err := parseFromDisk(bt.svgs, bt.files) if err != nil { return fmt.Errorf("Failed to parse template file: %w", err) } @@ -50,7 +51,7 @@ func (bt *BuiltTemplate) executeTemplate(w io.Writer, data any) error { } } -func NewBuiltTemplate(embeddedFiles embed.FS, subdir string, files ...string) *BuiltTemplate { +func NewBuiltTemplate(embeddedFiles embed.FS, subdir string, svgs []string, files ...string) *BuiltTemplate { files_on_disk := true for _, f := range files { _, err := os.Stat(f) @@ -67,13 +68,15 @@ func NewBuiltTemplate(embeddedFiles embed.FS, subdir string, files ...string) *B result = BuiltTemplate{ files: files, subdir: subdir, + svgs: svgs, template: nil, } } else { result = BuiltTemplate{ files: files, subdir: subdir, - template: parseEmbedded(embeddedFiles, subdir, files), + svgs: svgs, + template: parseEmbedded(embeddedFiles, subdir, svgs, files), } } TemplatesByFilename[path.Base(files[0])] = result @@ -125,7 +128,7 @@ func makeFuncMap() template.FuncMap { } return funcMap } -func parseEmbedded(embeddedFiles embed.FS, subdir string, files []string) *template.Template { +func parseEmbedded(embeddedFiles embed.FS, subdir string, svgs []string, files []string) *template.Template { funcMap := makeFuncMap() // Remap the file names to embedded paths embeddedFilePaths := make([]string, 0) @@ -134,11 +137,31 @@ func parseEmbedded(embeddedFiles embed.FS, subdir string, files []string) *templ } name := path.Base(embeddedFilePaths[0]) log.Debug().Str("name", name).Strs("paths", embeddedFilePaths).Msg("Parsing embedded template") - return template.Must( - template.New(name).Funcs(funcMap).ParseFS(embeddedFiles, embeddedFilePaths...)) + t, err := template.New(name).Funcs(funcMap).ParseFS(embeddedFiles, embeddedFilePaths...) + if err != nil { + panic(fmt.Sprintf("Failed to parse embedded template %s: %v", name, err)) + } + for _, svg := range svgs { + svg_path := strings.TrimPrefix(svg, subdir) + content, err := embeddedFiles.ReadFile(svg_path) + if err != nil { + panic(fmt.Sprintf("Failed to read svg '%s' from embedded filesystem: %v", svg, err)) + } + svg_name := path.Base(svg) + svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content)) + svg_t, err := template.New(svg_name).Parse(svg_template) + if err != nil { + panic(fmt.Sprintf("Failed to parse svg '%s' from embedded filesystem: %v", svg, err)) + } + _, err = t.AddParseTree(svg_t.Name(), svg_t.Tree) + if err != nil { + panic(fmt.Sprintf("Failed to add svg '%s' to embedded template: %v", svg, err)) + } + } + return t } -func parseFromDisk(files []string) (*template.Template, error) { +func parseFromDisk(svgs []string, files []string) (*template.Template, error) { funcMap := makeFuncMap() name := path.Base(files[0]) //log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk") @@ -146,6 +169,24 @@ func parseFromDisk(files []string) (*template.Template, error) { if err != nil { return nil, fmt.Errorf("Failed to parse %s: %w", files, err) } + for _, svg := range svgs { + content, err := os.ReadFile(svg) + if err != nil { + return nil, fmt.Errorf("Failed to read svg '%s' from filesystem: %w", svg, err) + } + svg_name := path.Base(svg) + svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content)) + svg_t, err := template.New(svg_name).Parse(svg_template) + if err != nil { + log.Debug().Str("svg", svg).Str("svg_name", svg_name).Str("template", svg_template).Msg("failed to parse") + return nil, fmt.Errorf("Failed to parse svg '%s' from filesystem: %w", svg, err) + } + _, err = templ.AddParseTree(svg_t.Name(), svg_t.Tree) + if err != nil { + return nil, fmt.Errorf("Failed to add svg '%s' to template: %w", svg, err) + } + log.Debug().Str("name", svg_t.Name()).Str("svg_name", svg_name).Msg("Added svg template") + } return templ, nil } diff --git a/rmo/template.go b/rmo/template.go index f7db337d..d86ecd1d 100644 --- a/rmo/template.go +++ b/rmo/template.go @@ -22,8 +22,9 @@ func buildTemplate(files ...string) *html.BuiltTemplate { for _, c := range components { full_files = append(full_files, fmt.Sprintf("%s/template/component/%s.html", subdir, c)) } + full_svgs := make([]string, 0) for _, c := range svgs { - full_files = append(full_files, fmt.Sprintf("%s/template/svg/%s.svg", subdir, c)) + full_svgs = append(full_svgs, fmt.Sprintf("%s/template/svg/%s.svg", subdir, c)) } - return html.NewBuiltTemplate(embeddedFiles, "rmo/", full_files...) + return html.NewBuiltTemplate(embeddedFiles, "rmo/", full_svgs, full_files...) } diff --git a/rmo/template/root.html b/rmo/template/root.html index d55a39bb..43d1da62 100644 --- a/rmo/template/root.html +++ b/rmo/template/root.html @@ -66,9 +66,7 @@
- - - + {{ template "mosquito.svg" }}

Follow-up or Check Status

Check on a previous request or view current mosquito activity in your area.

@@ -82,9 +80,7 @@
- - - + {{ template "pond.svg" }}

Report a Green Pool

Report stagnant water sources like abandoned pools that may breed mosquitoes.

@@ -98,9 +94,7 @@
- - - + {{ template "check-report.svg" }}

Report Mosquito Nuisance

Report areas with high adult mosquito activity causing discomfort or concern.

diff --git a/rmo/template/svg/check-report.svg b/rmo/template/svg/check-report.svg index 15b82fd6..bc645483 100644 --- a/rmo/template/svg/check-report.svg +++ b/rmo/template/svg/check-report.svg @@ -1,3 +1 @@ -{{define "svg/check-report"}} -{{end}} diff --git a/rmo/template/svg/mosquito.svg b/rmo/template/svg/mosquito.svg index ccbcec39..a19b8b23 100644 --- a/rmo/template/svg/mosquito.svg +++ b/rmo/template/svg/mosquito.svg @@ -1,3 +1 @@ -{{define "svg/mosquito"}} -{{end}} diff --git a/rmo/template/svg/pond.svg b/rmo/template/svg/pond.svg index bffa55de..d44728ee 100644 --- a/rmo/template/svg/pond.svg +++ b/rmo/template/svg/pond.svg @@ -1,3 +1 @@ -{{define "svg/pond"}} -{{end}} diff --git a/sync/page.go b/sync/page.go index 50d812ee..8cd7b1c0 100644 --- a/sync/page.go +++ b/sync/page.go @@ -23,7 +23,7 @@ func buildTemplate(files ...string) *html.BuiltTemplate { for _, c := range components { full_files = append(full_files, fmt.Sprintf("%s/template/components/%s.html", subdir, c)) } - return html.NewBuiltTemplate(embeddedFiles, "sync/", full_files...) + return html.NewBuiltTemplate(embeddedFiles, "sync/", []string{}, full_files...) } // Respond with an error that is visible to the user From 40028744bad0beb2a33d3a514bd8ea7d1be259a3 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 20:11:17 +0000 Subject: [PATCH 0204/1513] Fix svg pipeline to not need an explicit list --- html/html.go | 83 +++++++++++++++++++------------------ html/static.go | 2 - rmo/template.go | 7 +--- rmo/template/mock/root.html | 6 +-- sync/page.go | 2 +- 5 files changed, 47 insertions(+), 53 deletions(-) diff --git a/html/html.go b/html/html.go index c3b99fa6..47116935 100644 --- a/html/html.go +++ b/html/html.go @@ -7,6 +7,7 @@ import ( "fmt" "html/template" "io" + "io/fs" "math" "net/http" "os" @@ -26,7 +27,6 @@ var TemplatesByFilename = make(map[string]BuiltTemplate, 0) type BuiltTemplate struct { files []string subdir string - svgs []string // Nil if we are going to read templates off disk every time we render // because we are in development mode. template *template.Template @@ -35,7 +35,7 @@ type BuiltTemplate struct { func (bt *BuiltTemplate) executeTemplate(w io.Writer, data any) error { if bt.template == nil { name := path.Base(bt.files[0]) - templ, err := parseFromDisk(bt.svgs, bt.files) + templ, err := parseFromDisk(bt.subdir, bt.files) if err != nil { return fmt.Errorf("Failed to parse template file: %w", err) } @@ -51,7 +51,7 @@ func (bt *BuiltTemplate) executeTemplate(w io.Writer, data any) error { } } -func NewBuiltTemplate(embeddedFiles embed.FS, subdir string, svgs []string, files ...string) *BuiltTemplate { +func NewBuiltTemplate(embeddedFiles embed.FS, subdir string, files ...string) *BuiltTemplate { files_on_disk := true for _, f := range files { _, err := os.Stat(f) @@ -68,15 +68,13 @@ func NewBuiltTemplate(embeddedFiles embed.FS, subdir string, svgs []string, file result = BuiltTemplate{ files: files, subdir: subdir, - svgs: svgs, template: nil, } } else { result = BuiltTemplate{ files: files, subdir: subdir, - svgs: svgs, - template: parseEmbedded(embeddedFiles, subdir, svgs, files), + template: parseEmbedded(embeddedFiles, subdir, files), } } TemplatesByFilename[path.Base(files[0])] = result @@ -128,7 +126,32 @@ func makeFuncMap() template.FuncMap { } return funcMap } -func parseEmbedded(embeddedFiles embed.FS, subdir string, svgs []string, files []string) *template.Template { +func addSVGTemplates(fsys fs.FS, templ *template.Template) error { + svgs, err := fs.ReadDir(fsys, ".") + if err != nil { + log.Warn().Msg("Failed to read svg directory") + return nil + } + for _, svg := range svgs { + content, err := fs.ReadFile(fsys, svg.Name()) + if err != nil { + return fmt.Errorf("Failed to read svg '%s' from embedded filesystem: %w", svg, err) + } + svg_name := svg.Name() + svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content)) + svg_t, err := template.New(svg_name).Parse(svg_template) + if err != nil { + return fmt.Errorf("Failed to parse svg '%s' from embedded filesystem: %v", svg, err) + } + _, err = templ.AddParseTree(svg_t.Name(), svg_t.Tree) + if err != nil { + return fmt.Errorf("Failed to add svg '%s' to embedded template: %v", svg, err) + } + log.Debug().Str("name", svg_name).Msg("add svg template") + } + return nil +} +func parseEmbedded(embeddedFiles embed.FS, subdir string, files []string) *template.Template { funcMap := makeFuncMap() // Remap the file names to embedded paths embeddedFilePaths := make([]string, 0) @@ -141,27 +164,18 @@ func parseEmbedded(embeddedFiles embed.FS, subdir string, svgs []string, files [ if err != nil { panic(fmt.Sprintf("Failed to parse embedded template %s: %v", name, err)) } - for _, svg := range svgs { - svg_path := strings.TrimPrefix(svg, subdir) - content, err := embeddedFiles.ReadFile(svg_path) - if err != nil { - panic(fmt.Sprintf("Failed to read svg '%s' from embedded filesystem: %v", svg, err)) - } - svg_name := path.Base(svg) - svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content)) - svg_t, err := template.New(svg_name).Parse(svg_template) - if err != nil { - panic(fmt.Sprintf("Failed to parse svg '%s' from embedded filesystem: %v", svg, err)) - } - _, err = t.AddParseTree(svg_t.Name(), svg_t.Tree) - if err != nil { - panic(fmt.Sprintf("Failed to add svg '%s' to embedded template: %v", svg, err)) - } + svg_fs, err := fs.Sub(embeddedFiles, "template/svg") + if err != nil { + panic(fmt.Sprintf("Failed to read static/svg: %v", err)) + } + err = addSVGTemplates(svg_fs, t) + if err != nil { + panic(fmt.Sprintf("Failed to add SVG templates: %v", err)) } return t } -func parseFromDisk(svgs []string, files []string) (*template.Template, error) { +func parseFromDisk(subdir string, files []string) (*template.Template, error) { funcMap := makeFuncMap() name := path.Base(files[0]) //log.Debug().Str("name", name).Strs("files", files).Msg("parsing from disk") @@ -169,23 +183,10 @@ func parseFromDisk(svgs []string, files []string) (*template.Template, error) { if err != nil { return nil, fmt.Errorf("Failed to parse %s: %w", files, err) } - for _, svg := range svgs { - content, err := os.ReadFile(svg) - if err != nil { - return nil, fmt.Errorf("Failed to read svg '%s' from filesystem: %w", svg, err) - } - svg_name := path.Base(svg) - svg_template := fmt.Sprintf("{{define \"%s\"}}%s{{end}}", svg_name, string(content)) - svg_t, err := template.New(svg_name).Parse(svg_template) - if err != nil { - log.Debug().Str("svg", svg).Str("svg_name", svg_name).Str("template", svg_template).Msg("failed to parse") - return nil, fmt.Errorf("Failed to parse svg '%s' from filesystem: %w", svg, err) - } - _, err = templ.AddParseTree(svg_t.Name(), svg_t.Tree) - if err != nil { - return nil, fmt.Errorf("Failed to add svg '%s' to template: %w", svg, err) - } - log.Debug().Str("name", svg_t.Name()).Str("svg_name", svg_name).Msg("Added svg template") + fsys := os.DirFS(subdir + "/template/svg") + err = addSVGTemplates(fsys, templ) + if err != nil { + return nil, fmt.Errorf("Failed to add SVGs from disk: %w", err) } return templ, nil } diff --git a/html/static.go b/html/static.go index c6fbb429..4f4818a0 100644 --- a/html/static.go +++ b/html/static.go @@ -2,11 +2,9 @@ package html import ( "embed" - "io/fs" "net/http" "github.com/go-chi/chi/v5" - "github.com/rs/zerolog/log" ) //go:embed static/* diff --git a/rmo/template.go b/rmo/template.go index d86ecd1d..3238b81b 100644 --- a/rmo/template.go +++ b/rmo/template.go @@ -11,7 +11,6 @@ import ( var embeddedFiles embed.FS var components = [...]string{"footer", "header", "photo-upload", "photo-upload-header"} -var svgs = [...]string{"check-report", "mosquito", "pond"} func buildTemplate(files ...string) *html.BuiltTemplate { subdir := "rmo" @@ -22,9 +21,5 @@ func buildTemplate(files ...string) *html.BuiltTemplate { for _, c := range components { full_files = append(full_files, fmt.Sprintf("%s/template/component/%s.html", subdir, c)) } - full_svgs := make([]string, 0) - for _, c := range svgs { - full_svgs = append(full_svgs, fmt.Sprintf("%s/template/svg/%s.svg", subdir, c)) - } - return html.NewBuiltTemplate(embeddedFiles, "rmo/", full_svgs, full_files...) + return html.NewBuiltTemplate(embeddedFiles, "rmo/", full_files...) } diff --git a/rmo/template/mock/root.html b/rmo/template/mock/root.html index ff6c712c..232fe125 100644 --- a/rmo/template/mock/root.html +++ b/rmo/template/mock/root.html @@ -56,7 +56,7 @@
- {{ template "svg/mosquito" }} + {{ template "mosquito.svg" }}

Report Mosquito Nuisance

Report areas with high adult mosquito activity causing discomfort or concern.

@@ -68,7 +68,7 @@
- {{ template "svg/pond" }} + {{ template "pond.svg" }}

Report Standing Water

Report any water that has been sitting for several days, where mosquitoes can live.

@@ -80,7 +80,7 @@
- {{ template "svg/check-report" }} + {{ template "check-report.svg" }}

Follow-up or Check Status

Check on a previous request or view current mosquito activity in your area.

diff --git a/sync/page.go b/sync/page.go index 8cd7b1c0..50d812ee 100644 --- a/sync/page.go +++ b/sync/page.go @@ -23,7 +23,7 @@ func buildTemplate(files ...string) *html.BuiltTemplate { for _, c := range components { full_files = append(full_files, fmt.Sprintf("%s/template/components/%s.html", subdir, c)) } - return html.NewBuiltTemplate(embeddedFiles, "sync/", []string{}, full_files...) + return html.NewBuiltTemplate(embeddedFiles, "sync/", full_files...) } // Respond with an error that is visible to the user From 7fb02f478833f322926926e9ddde927ee501f877 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 20:14:10 +0000 Subject: [PATCH 0205/1513] Update root RMO to use banner and new colored icon --- rmo/template/mock/root.html | 34 ----------- rmo/template/root.html | 91 +++++------------------------ rmo/template/svg/mosquito-color.svg | 1 + scss/custom.scss | 1 + scss/rmo/root.scss | 32 ++++++++++ 5 files changed, 47 insertions(+), 112 deletions(-) create mode 100644 rmo/template/svg/mosquito-color.svg create mode 100644 scss/rmo/root.scss diff --git a/rmo/template/mock/root.html b/rmo/template/mock/root.html index 232fe125..5f0f8df9 100644 --- a/rmo/template/mock/root.html +++ b/rmo/template/mock/root.html @@ -2,40 +2,6 @@ {{define "title"}}Main{{end}} {{define "extraheader"}} - {{end}} {{define "content"}} diff --git a/rmo/template/root.html b/rmo/template/root.html index 43d1da62..80aed492 100644 --- a/rmo/template/root.html +++ b/rmo/template/root.html @@ -2,57 +2,14 @@ {{define "title"}}Main{{end}} {{define "extraheader"}} - {{end}} {{define "content"}}
-
-
-
-

Report Mosquitoes Online

-

- We are dedicated to protecting public health and improving quality of life by reducing - mosquito populations and the diseases they can carry. Our districts provide comprehensive - mosquito surveillance, control, and education services to our community. -

-
-
-
-
- - -
-
-
-
-

On the go?

- Make a Quick Report -

Report mosquito issues in under 60 seconds

-
-
+
@@ -61,67 +18,45 @@

How Can We Help You Today?

- - -
{{ template "pond.svg" }}
-

Report a Green Pool

-

Report stagnant water sources like abandoned pools that may breed mosquitoes.

- Report Source +

Report Standing Water

+

Report any water that has been sitting for several days, where mosquitoes can live.

+ Report Source
- -
{{ template "check-report.svg" }}
-

Report Mosquito Nuisance

-

Report areas with high adult mosquito activity causing discomfort or concern.

- Report Problem +

Follow-up or Check Status

+

Check on a previous request or view current mosquito activity in your area.

+ Get Status
+
- -
-
-
-
-
-
-
Need to make a quick report?
-

Use our streamlined form to report mosquito issues in under 60 seconds

-
- -
-
-
-
-
diff --git a/rmo/template/svg/mosquito-color.svg b/rmo/template/svg/mosquito-color.svg new file mode 100644 index 00000000..00b251d7 --- /dev/null +++ b/rmo/template/svg/mosquito-color.svg @@ -0,0 +1 @@ + diff --git a/scss/custom.scss b/scss/custom.scss index b69de586..4edb4fde 100644 --- a/scss/custom.scss +++ b/scss/custom.scss @@ -41,3 +41,4 @@ $theme-colors: map-merge( @import "./bootstrap/scss/bootstrap"; @import "./sidebar.scss"; +@import "./rmo/root.scss"; diff --git a/scss/rmo/root.scss b/scss/rmo/root.scss new file mode 100644 index 00000000..5606b41a --- /dev/null +++ b/scss/rmo/root.scss @@ -0,0 +1,32 @@ +.service-card { + transition: transform 0.3s; + height: 100%; +} +.service-card:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0,0,0,0.1); +} +.district-logo { + max-height: 80px; + width: auto; +} +.quick-report-mobile { + background-color: #ff9800; +} +.quick-report-desktop { + background-color: #ffefd5; + border-left: 4px solid #ff9800; +} + +.banner-container { + position: relative; + width: 100%; + background-color: #F76436; + overflow: hidden; +} + +.banner-image { + display: block; + /* width as needed */ +} + From 48c49fc73e4aadf423c603b7cc52128d5f5a4bf0 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 20:17:29 +0000 Subject: [PATCH 0206/1513] Use colored svgs for pond and status on RMO --- rmo/template/root.html | 4 ++-- rmo/template/svg/check-report-color.svg | 1 + rmo/template/svg/pond-color.svg | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 rmo/template/svg/check-report-color.svg create mode 100644 rmo/template/svg/pond-color.svg diff --git a/rmo/template/root.html b/rmo/template/root.html index 80aed492..23d1de93 100644 --- a/rmo/template/root.html +++ b/rmo/template/root.html @@ -34,7 +34,7 @@
- {{ template "pond.svg" }} + {{ template "pond-color.svg" }}

Report Standing Water

Report any water that has been sitting for several days, where mosquitoes can live.

@@ -46,7 +46,7 @@
- {{ template "check-report.svg" }} + {{ template "check-report-color.svg" }}

Follow-up or Check Status

Check on a previous request or view current mosquito activity in your area.

diff --git a/rmo/template/svg/check-report-color.svg b/rmo/template/svg/check-report-color.svg new file mode 100644 index 00000000..4d98b2e0 --- /dev/null +++ b/rmo/template/svg/check-report-color.svg @@ -0,0 +1 @@ + diff --git a/rmo/template/svg/pond-color.svg b/rmo/template/svg/pond-color.svg new file mode 100644 index 00000000..22383a84 --- /dev/null +++ b/rmo/template/svg/pond-color.svg @@ -0,0 +1 @@ + From bab8af4572f015e6aa86f968a92958596af015b1 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 30 Jan 2026 20:41:02 +0000 Subject: [PATCH 0207/1513] Get basic bones of the nuisance page copied from the mock --- rmo/mock.go | 5 - rmo/nuisance.go | 17 +- rmo/root.go | 15 +- rmo/routes.go | 8 +- rmo/template.go | 2 +- .../{header.html => header-district.html} | 6 +- rmo/template/component/header-rmo.html | 12 + rmo/template/mock/district-root.html | 6 +- rmo/template/mock/nuisance.html | 95 +-- rmo/template/nuisance.html | 563 +++++++----------- scss/custom.scss | 1 + scss/rmo/nuisance.scss | 93 +++ 12 files changed, 371 insertions(+), 452 deletions(-) rename rmo/template/component/{header.html => header-district.html} (62%) create mode 100644 rmo/template/component/header-rmo.html create mode 100644 scss/rmo/nuisance.scss diff --git a/rmo/mock.go b/rmo/mock.go index 7d6a6599..3e6874ce 100644 --- a/rmo/mock.go +++ b/rmo/mock.go @@ -17,11 +17,6 @@ var ( mockWaterT = buildTemplate("mock/water", "base") ) -type ContentDistrict struct { - Name string - URLLogo string - URLWebsite string -} type ContentMock struct { District ContentDistrict MapboxToken string diff --git a/rmo/nuisance.go b/rmo/nuisance.go index 5580daa4..3a66d23c 100644 --- a/rmo/nuisance.go +++ b/rmo/nuisance.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/Gleipnir-Technology/nidus-sync/config" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" @@ -15,8 +16,12 @@ import ( "github.com/rs/zerolog/log" ) -type ContextNuisance struct{} -type ContextNuisanceSubmitComplete struct { +type ContentNuisance struct { + District *ContentDistrict + MapboxToken string + URL ContentURL +} +type ContentNuisanceSubmitComplete struct { ReportID string } @@ -29,7 +34,11 @@ func getNuisance(w http.ResponseWriter, r *http.Request) { html.RenderOrError( w, Nuisance, - ContextNuisance{}, + ContentNuisance{ + District: nil, + MapboxToken: config.MapboxToken, + URL: makeContentURL(), + }, ) } func getNuisanceSubmitComplete(w http.ResponseWriter, r *http.Request) { @@ -37,7 +46,7 @@ func getNuisanceSubmitComplete(w http.ResponseWriter, r *http.Request) { html.RenderOrError( w, NuisanceSubmitComplete, - ContextNuisanceSubmitComplete{ + ContentNuisanceSubmitComplete{ ReportID: report, }, ) diff --git a/rmo/root.go b/rmo/root.go index b9edf4ab..a878e6ce 100644 --- a/rmo/root.go +++ b/rmo/root.go @@ -9,6 +9,11 @@ import ( "github.com/rs/zerolog/log" ) +type ContentDistrict struct { + Name string + URLLogo string + URLWebsite string +} type ContentPrivacy struct { Address string Company string @@ -56,7 +61,9 @@ func getRoot(w http.ResponseWriter, r *http.Request) { html.RenderOrError( w, RootT, - ContentRoot{}, + ContentRoot{ + URL: makeContentURL(), + }, ) } @@ -68,11 +75,13 @@ func getTerms(w http.ResponseWriter, r *http.Request) { html.RenderOrError( w, TermsT, - ContentRoot{}, + ContentRoot{ + URL: makeContentURL(), + }, ) } -func makeContentURL(slug string) ContentURL { +func makeContentURL() ContentURL { return ContentURL{ Nuisance: makeURL("nuisance"), NuisanceSubmitComplete: makeURL("nuisance-submit-complete"), diff --git a/rmo/routes.go b/rmo/routes.go index 415fb836..41662d7e 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -8,12 +8,18 @@ import ( func Router() chi.Router { r := chi.NewRouter() r.Get("/", getRoot) + r.Get("/nuisance", getNuisance) + //r.Get("/district/{slug}", renderMock(mockDistrictRootT)) + //r.Get("/district/{slug}/nuisance", renderMock(mockNuisanceT)) + //r.Get("/district/{slug}/nuisance-submit-complete", renderMock(mockNuisanceSubmitCompleteT)) + //r.Get("/district/{slug}/status", renderMock(mockStatusT)) + //r.Get("/district/{slug}/water", renderMock(mockWaterT)) + r.Get("/privacy", getPrivacy) r.Get("/robots.txt", getRobots) r.Get("/email", getEmailByCode) r.Get("/image/{uuid}", getImageByUUID) r.Route("/mock", addMockRoutes) - r.Get("/nuisance", getNuisance) r.Post("/nuisance-submit", postNuisance) r.Get("/nuisance-submit-complete", getNuisanceSubmitComplete) r.Get("/pool", getPool) diff --git a/rmo/template.go b/rmo/template.go index 3238b81b..470c2596 100644 --- a/rmo/template.go +++ b/rmo/template.go @@ -10,7 +10,7 @@ import ( //go:embed template/* var embeddedFiles embed.FS -var components = [...]string{"footer", "header", "photo-upload", "photo-upload-header"} +var components = [...]string{"footer", "header-district", "header-rmo", "photo-upload", "photo-upload-header"} func buildTemplate(files ...string) *html.BuiltTemplate { subdir := "rmo" diff --git a/rmo/template/component/header.html b/rmo/template/component/header-district.html similarity index 62% rename from rmo/template/component/header.html rename to rmo/template/component/header-district.html index 6679d739..b80301b8 100644 --- a/rmo/template/component/header.html +++ b/rmo/template/component/header-district.html @@ -1,11 +1,11 @@ -{{define "header"}} +{{define "header-district"}} diff --git a/rmo/template/component/header-rmo.html b/rmo/template/component/header-rmo.html new file mode 100644 index 00000000..912b6bba --- /dev/null +++ b/rmo/template/component/header-rmo.html @@ -0,0 +1,12 @@ +{{define "header-rmo"}} + + +{{end}} diff --git a/rmo/template/mock/district-root.html b/rmo/template/mock/district-root.html index f3069987..9a32b37c 100644 --- a/rmo/template/mock/district-root.html +++ b/rmo/template/mock/district-root.html @@ -53,7 +53,7 @@
- {{ template "svg/mosquito" }} + {{ template "mosquito-color.svg" }}

Report Mosquito Nuisance

Report areas with high adult mosquito activity causing discomfort or concern.

@@ -65,7 +65,7 @@
- {{ template "svg/pond" }} + {{ template "pond-color.svg" }}

Report Standing Water

Report any water that has been sitting for several days, where mosquitoes can live.

@@ -77,7 +77,7 @@
- {{ template "svg/check-report" }} + {{ template "check-report-color.svg" }}

Follow-up or Check Status

Check on a previous request or view current mosquito activity in your area.

diff --git a/rmo/template/mock/nuisance.html b/rmo/template/mock/nuisance.html index aa2f88b6..83d02910 100644 --- a/rmo/template/mock/nuisance.html +++ b/rmo/template/mock/nuisance.html @@ -138,99 +138,6 @@ document.addEventListener('DOMContentLoaded', function() { }); {{end}} {{define "content"}} @@ -275,7 +182,7 @@ select.tall {
- {{ template "svg/mosquito" }} + {{ template "mosquito-color.svg" }}

Mosquito Activity Information

The time when mosquitoes are active can help us identify the species and likely breeding sources.

diff --git a/rmo/template/nuisance.html b/rmo/template/nuisance.html index ccc5ee93..1f1a9e31 100644 --- a/rmo/template/nuisance.html +++ b/rmo/template/nuisance.html @@ -2,7 +2,14 @@ {{define "title"}}Nuisance{{end}} {{define "extraheader"}} + + + + + + {{end}} {{define "content"}} +{{if (eq .District nil)}} + {{template "header-rmo" .}} +{{else}} + {{template "header-district" .District}} +{{end}}
-

Report Mosquito Nuisance

@@ -152,24 +154,38 @@ document.addEventListener('DOMContentLoaded', function() {
- -
-
- -
-
- + +
+
+ +

Nuisance Location Information

+
+
+
+

You can select the location by address or by moving the marker on the map.

+
+
+ +
+
+ + +
+
+
+

You can also click on the map to mark the location precisely

+ + +
- + {{ template "mosquito-color.svg" }}

Mosquito Activity Information

- optional

The time when mosquitoes are active can help us identify the species and likely breeding sources.

@@ -227,260 +243,131 @@ document.addEventListener('DOMContentLoaded', function() {
- - -
- - -
-
-
Minor
- Occasional mosquito -
-
-
Moderate
- Regular presence -
-
-
Severe
- Many mosquitoes -
-
-
- Current selection: 3/5 -
-
- - - -
- -
-
- -

Potential Mosquito Sources

- optional -
-

Have you noticed any of these common mosquito breeding sources in your area?

- -
-
-
Did you know?
-

Mosquitoes can breed in as little as a bottle cap of water! Eliminating standing water is the most effective way to reduce mosquito populations.

+ +
+ +
+
+ +

Potential Mosquito Sources

-
+

Have you noticed any of these common mosquito breeding sources in your area?

-
- -
-
-
-
- -
-
Stagnant Water
-

Green pools, ponds, fountains, or birdbaths that aren't maintained

-
- - -
-
-
-
- - -
-
-
-
- -
-
Containers
-

Buckets, planters, toys, tires, or any items that collect rainwater

-
- - -
-
-
-
- - -
-
-
-
- -
-
Roof & Gutters
-

Clogged gutters, flat roofs, or AC units that collect water

-
- - -
-
-
-
-
- - - -
-
- - -
-
-
- - -
-
- -

Inspection Request

-
-

Would you like our technicians to inspect for potential mosquito sources?

- -
-
-
-
Property Inspection
-

Request a technician to inspect your property for mosquito sources. We'll contact you to schedule a convenient time.

-
- - -
-
-
- -
-
-
Neighborhood Inspection
-

Request a general inspection of your neighborhood. We'll survey the area for potential mosquito breeding sources.

-
- - -
-
-
-
- - -
From b77d9aa80a2c11278e62581c3df080e401c433ed Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 2 Feb 2026 07:14:03 +0000 Subject: [PATCH 0246/1513] Correctly set location data after reverse geocode This fixes the location when a marker drag finishes --- rmo/template/nuisance.html | 1 + 1 file changed, 1 insertion(+) diff --git a/rmo/template/nuisance.html b/rmo/template/nuisance.html index f77bac98..47839138 100644 --- a/rmo/template/nuisance.html +++ b/rmo/template/nuisance.html @@ -19,6 +19,7 @@ async function handleMarkerDrag(lngLat) { if (response !== undefined && response.features.length > 0) { const addressInput = document.querySelector("address-input"); addressInput.SetValue(response.features[0]); + setLocationInputs(response.features[0]); } } function selectInspectionType(type) { From 7ee2f72b8ea44ac46daa512343e028af2386d4fb Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 2 Feb 2026 14:23:22 +0000 Subject: [PATCH 0247/1513] Add district list to report mosquitoes online Makes it easier at a conference to find the district we're talking to --- rmo/district.go | 36 +++++++++++++++++++++++++++++++++ rmo/routes.go | 1 + rmo/template/district-list.html | 28 +++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 rmo/template/district-list.html diff --git a/rmo/district.go b/rmo/district.go index 3816e3b6..e33ddf9d 100644 --- a/rmo/district.go +++ b/rmo/district.go @@ -3,17 +3,28 @@ package rmo import ( "net/http" + "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" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/go-chi/chi/v5" ) type ContentDistrict struct { Name string URLLogo string + URLRMO string URLWebsite string } +type ContentDistrictList struct { + Districts []ContentDistrict + URL ContentURL +} + +var ( + DistrictListT = buildTemplate("district-list", "base") +) func districtBySlug(r *http.Request) (*models.Organization, error) { slug := chi.URLParam(r, "slug") @@ -22,6 +33,30 @@ func districtBySlug(r *http.Request) (*models.Organization, error) { ).One(r.Context(), db.PGInstance.BobDB) return district, err } +func getDistrictList(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + rows, err := models.Organizations.Query( + models.SelectWhere.Organizations.ImportDistrictGid.IsNotNull(), + sm.OrderBy("name"), + ).All(ctx, db.PGInstance.BobDB) + if err != nil { + respondError(w, "failed to query for districts", err, http.StatusInternalServerError) + return + } + districts := make([]ContentDistrict, 0) + for _, row := range rows { + districts = append(districts, *newContentDistrict(row)) + } + html.RenderOrError( + w, + DistrictListT, + ContentDistrictList{ + Districts: districts, + URL: makeContentURL(nil), + }, + ) + +} func newContentDistrict(d *models.Organization) *ContentDistrict { if d == nil { return nil @@ -29,6 +64,7 @@ func newContentDistrict(d *models.Organization) *ContentDistrict { return &ContentDistrict{ Name: d.Name, URLLogo: config.MakeURLNidus("/api/district/%s/logo", d.Slug.GetOr("unset")), + URLRMO: config.MakeURLReport("/district/%s", d.Slug.GetOr("unset")), URLWebsite: d.Website.GetOr(""), } } diff --git a/rmo/routes.go b/rmo/routes.go index d672d263..cace372c 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -14,6 +14,7 @@ func Router() chi.Router { r.Get("/water", getWater) r.Post("/water", postWater) + r.Get("/district", getDistrictList) r.Get("/district/{slug}", getRootDistrict) r.Get("/district/{slug}/nuisance", getNuisanceDistrict) //r.Get("/district/{slug}/nuisance-submit-complete", renderMock(mockNuisanceSubmitCompleteT)) diff --git a/rmo/template/district-list.html b/rmo/template/district-list.html new file mode 100644 index 00000000..d53eab39 --- /dev/null +++ b/rmo/template/district-list.html @@ -0,0 +1,28 @@ +{{template "base.html" .}} + +{{define "title"}}Districts{{end}} +{{define "extraheader"}} +{{end}} +{{define "content"}} + +
+

District List

+ + + + + + + + {{ range .Districts }} + + + + + + {{ end }} + +
LogoNameURL
{{.Name}}{{.URLRMO}}
+
+ +{{end}} From 00a75a556e904dd6ede07357dee6a3a63bf6f61a Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 2 Feb 2026 17:00:48 +0000 Subject: [PATCH 0248/1513] Fix email sending for report notification confirmation The links in the email don't work, but it's a first step --- background/email.go | 3 +- comms/email/job.go | 9 +- .../email/report_notification_confirmation.go | 85 +++++++++++++++++++ .../email/report_subscription_confirmation.go | 80 ----------------- comms/email/template.go | 9 +- ... => report-notification-confirmation.html} | 2 +- ...t => report-notification-confirmation.txt} | 0 db/enums/enums.bob.go | 5 +- ...1_messagetypeemail_report_notification.sql | 3 + 9 files changed, 105 insertions(+), 91 deletions(-) create mode 100644 comms/email/report_notification_confirmation.go delete mode 100644 comms/email/report_subscription_confirmation.go rename comms/email/template/{report-subscription-confirmation.html => report-notification-confirmation.html} (95%) rename comms/email/template/{report-subscription-confirmation.txt => report-notification-confirmation.txt} (100%) create mode 100644 db/migrations/00051_messagetypeemail_report_notification.sql diff --git a/background/email.go b/background/email.go index ccdd464c..352adabb 100644 --- a/background/email.go +++ b/background/email.go @@ -10,7 +10,7 @@ import ( var channelJobEmail chan email.Job func ReportSubscriptionConfirmationEmail(destination, report_id string) { - enqueueJobEmail(email.NewJobReportSubscriptionConfirmation( + enqueueJobEmail(email.NewJobReportNotificationConfirmation( destination, report_id, )) @@ -36,6 +36,7 @@ func startWorkerEmail(ctx context.Context, channel chan email.Job) { case job := <-channel: err := email.Handle(ctx, job) if err != nil { + log.Error().Err(err).Msg("Failed to handle email message") } } } diff --git a/comms/email/job.go b/comms/email/job.go index b2f3fec7..73f9f313 100644 --- a/comms/email/job.go +++ b/comms/email/job.go @@ -24,8 +24,11 @@ type jobEmailBase struct { func Handle(ctx context.Context, job Job) error { var err error + log.Debug().Str("dest", job.destination()).Str("type", string(job.messageType())).Msg("Handling email job") switch job.messageType() { case enums.CommsMessagetypeemailReportSubscriptionConfirmation: + return errors.New("ReportSubscription has been deprecated.") + case enums.CommsMessagetypeemailReportNotificationConfirmation: err = sendEmailReportConfirmation(ctx, job) default: return errors.New("not implemented") @@ -35,10 +38,4 @@ func Handle(ctx context.Context, job Job) error { return fmt.Errorf("Failed to handle email: %w", err) } return nil - /* - case enums.CommsMessagetypeemailReportStatusScheduled: - case enums.CommsMessagetypeemailReportStatusComplete: - - } - */ } diff --git a/comms/email/report_notification_confirmation.go b/comms/email/report_notification_confirmation.go new file mode 100644 index 00000000..efefd502 --- /dev/null +++ b/comms/email/report_notification_confirmation.go @@ -0,0 +1,85 @@ +package email + +import ( + "context" + "fmt" + + "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/db/enums" + "github.com/rs/zerolog/log" +) + +func NewJobReportNotificationConfirmation(destination, report_id string) Job { + return jobEmailReportNotificationConfirmation{ + dest: destination, + reportID: report_id, + } +} + +type jobEmailReportNotificationConfirmation struct { + dest string + reportID string +} + +func (job jobEmailReportNotificationConfirmation) destination() string { + return job.dest +} +func (job jobEmailReportNotificationConfirmation) messageType() enums.CommsMessagetypeemail { + return enums.CommsMessagetypeemailReportNotificationConfirmation +} +func (job jobEmailReportNotificationConfirmation) renderHTML() (string, error) { + _ = newContentEmailNotificationConfirmation(job) + return "", nil +} +func (job jobEmailReportNotificationConfirmation) renderTXT() (string, error) { + return "fake txt", nil +} +func (job jobEmailReportNotificationConfirmation) subject() string { + return "" +} + +func sendEmailReportConfirmation(ctx context.Context, job Job) error { + j, ok := job.(jobEmailReportNotificationConfirmation) + if !ok { + return fmt.Errorf("job is not for report subscription confirmation") + } + err := maybeSendInitialEmail(ctx, j.destination()) + if err != nil { + return fmt.Errorf("Failed to handle initial email: %w", err) + } + data := make(map[string]string, 0) + public_id := generatePublicId(enums.CommsMessagetypeemailInitialContact, data) + data["report_id"] = j.reportID + report_id_str := publicReportID(j.reportID) + data["ReportIDStr"] = report_id_str + data["URLLogo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png") + data["URLReportStatus"] = config.MakeURLReport("/foo") + data["URLReportUnsubscribe"] = config.MakeURLReport("/email/unsubscribe") + data["URLViewInBrowser"] = config.MakeURLReport("/email?id=%s", public_id) + text, html, err := renderEmailTemplates(templateReportNotificationConfirmationID, data) + if err != nil { + return fmt.Errorf("Failed to render email report notification template: %w", err) + } + subject := fmt.Sprintf("Mosquito Report Submission - %s", report_id_str) + err = insertEmailLog(ctx, data, j.destination(), public_id, config.ForwardEmailReportAddress, subject, templateReportNotificationConfirmationID) + if err != nil { + return fmt.Errorf("Failed to store email log: %w", err) + } + resp, err := sendEmail(ctx, emailRequest{ + From: config.ForwardEmailReportAddress, + HTML: html, + Subject: subject, + Text: text, + To: j.destination(), + }, enums.CommsMessagetypeemailReportNotificationConfirmation) + if err != nil { + return fmt.Errorf("Failed to send email report confirmation to %s for report %s: %w", j.dest, j.reportID, err) + } + log.Info().Str("id", resp.ID).Str("dest", j.dest).Str("report_id", j.reportID).Msg("Sent report confirmation email") + return nil +} + +func newContentEmailNotificationConfirmation(job jobEmailReportNotificationConfirmation) (result contentEmailReportConfirmation) { + result.URLReportStatus = config.MakeURLReport("/status/%s", job.reportID) + return result +} diff --git a/comms/email/report_subscription_confirmation.go b/comms/email/report_subscription_confirmation.go deleted file mode 100644 index c689859d..00000000 --- a/comms/email/report_subscription_confirmation.go +++ /dev/null @@ -1,80 +0,0 @@ -package email - -import ( - "context" - "fmt" - - "github.com/Gleipnir-Technology/nidus-sync/config" - "github.com/Gleipnir-Technology/nidus-sync/db/enums" - //"github.com/rs/zerolog/log" -) - -func NewJobReportSubscriptionConfirmation(destination, report_id string) Job { - return jobEmailReportSubscriptionConfirmation{ - dest: destination, - reportID: report_id, - } -} - -type jobEmailReportSubscriptionConfirmation struct { - dest string - reportID string -} - -func (job jobEmailReportSubscriptionConfirmation) destination() string { - return job.dest -} -func (job jobEmailReportSubscriptionConfirmation) messageType() enums.CommsMessagetypeemail { - return enums.CommsMessagetypeemailReportSubscriptionConfirmation -} -func (job jobEmailReportSubscriptionConfirmation) renderHTML() (string, error) { - _ = newContentEmailSubscriptionConfirmation(job) - return "", nil -} -func (job jobEmailReportSubscriptionConfirmation) renderTXT() (string, error) { - return "fake txt", nil -} -func (job jobEmailReportSubscriptionConfirmation) subject() string { - return "" -} - -func sendEmailReportConfirmation(ctx context.Context, job Job) error { - j, ok := job.(jobEmailReportSubscriptionConfirmation) - if !ok { - return fmt.Errorf("job is not for report subscription confirmation") - } - err := maybeSendInitialEmail(ctx, j.destination()) - if err != nil { - return fmt.Errorf("Failed to handle initial email: %w", err) - } - return nil - /* - report_id_str := publicReportID(report_id) - content := newContentEmailSubscriptionConfirmation(report_id) - text, html, err := renderEmailTemplates(reportConfirmationT, content) - if err != nil { - return fmt.Errorf("Failed to render template %s: %w", reportConfirmationT.name, err) - } - resp, err := sendEmail(ctx, emailRequest{ - From: config.ForwardEmailReportAddress, - HTML: html, - Subject: fmt.Sprintf("Mosquito Report Submission - %s", report_id_str), - Text: text, - To: to, - }, enums.CommsMessagetypeemailReportSubscriptionConfirmation) - if err != nil { - return fmt.Errorf("Failed to send email report confirmation to %s for report %s: %w", to, report_id, err) - } - log.Info().Str("id", resp.ID).Str("to", to).Str("report_id", report_id).Msg("Sent report confirmation email") - return nil - */ -} - -func newContentEmailSubscriptionConfirmation(job jobEmailReportSubscriptionConfirmation) (result contentEmailReportConfirmation) { - /*newContentBase( - &result.Base, - config.MakeURLReport("/email/report/%s/subscription-confirmation", job.reportID), - )*/ - result.URLReportStatus = config.MakeURLReport("/status/%s", job.reportID) - return result -} diff --git a/comms/email/template.go b/comms/email/template.go index adb6446f..a59bbfe4 100644 --- a/comms/email/template.go +++ b/comms/email/template.go @@ -31,8 +31,9 @@ import ( var embeddedFiles embed.FS var ( - templateByID map[int32]*builtTemplate - templateInitialID int32 + templateByID map[int32]*builtTemplate + templateInitialID int32 + templateReportNotificationConfirmationID int32 ) type templatePair struct { @@ -81,6 +82,10 @@ func LoadTemplates() error { if err != nil { return fmt.Errorf("Failed to load template ID: %s", err) } + templateReportNotificationConfirmationID, err = loadTemplateID(ctx, tx, enums.CommsMessagetypeemailReportNotificationConfirmation) + if err != nil { + return fmt.Errorf("Failed to load report-notification-confirmation template ID: %s", err) + } tx.Commit(ctx) return nil } diff --git a/comms/email/template/report-subscription-confirmation.html b/comms/email/template/report-notification-confirmation.html similarity index 95% rename from comms/email/template/report-subscription-confirmation.html rename to comms/email/template/report-notification-confirmation.html index 88ef306a..0eef1e71 100644 --- a/comms/email/template/report-subscription-confirmation.html +++ b/comms/email/template/report-notification-confirmation.html @@ -76,7 +76,7 @@

Thank You for Your Report

-

We've received your mosquito report. Thanks! We appreciate you taking the time to submit it.

+

We've received your mosquito report {{.ReportIDStr}}. Thanks! We appreciate you taking the time to submit it.

You can check the current status of your report at any time by clicking the button below:

diff --git a/comms/email/template/report-subscription-confirmation.txt b/comms/email/template/report-notification-confirmation.txt similarity index 100% rename from comms/email/template/report-subscription-confirmation.txt rename to comms/email/template/report-notification-confirmation.txt diff --git a/db/enums/enums.bob.go b/db/enums/enums.bob.go index 14d12cac..77800db7 100644 --- a/db/enums/enums.bob.go +++ b/db/enums/enums.bob.go @@ -199,6 +199,7 @@ const ( CommsMessagetypeemailReportSubscriptionConfirmation CommsMessagetypeemail = "report-subscription-confirmation" CommsMessagetypeemailReportStatusScheduled CommsMessagetypeemail = "report-status-scheduled" CommsMessagetypeemailReportStatusComplete CommsMessagetypeemail = "report-status-complete" + CommsMessagetypeemailReportNotificationConfirmation CommsMessagetypeemail = "report-notification-confirmation" ) func AllCommsMessagetypeemail() []CommsMessagetypeemail { @@ -207,6 +208,7 @@ func AllCommsMessagetypeemail() []CommsMessagetypeemail { CommsMessagetypeemailReportSubscriptionConfirmation, CommsMessagetypeemailReportStatusScheduled, CommsMessagetypeemailReportStatusComplete, + CommsMessagetypeemailReportNotificationConfirmation, } } @@ -221,7 +223,8 @@ func (e CommsMessagetypeemail) Valid() bool { case CommsMessagetypeemailInitialContact, CommsMessagetypeemailReportSubscriptionConfirmation, CommsMessagetypeemailReportStatusScheduled, - CommsMessagetypeemailReportStatusComplete: + CommsMessagetypeemailReportStatusComplete, + CommsMessagetypeemailReportNotificationConfirmation: return true default: return false diff --git a/db/migrations/00051_messagetypeemail_report_notification.sql b/db/migrations/00051_messagetypeemail_report_notification.sql new file mode 100644 index 00000000..fd85441b --- /dev/null +++ b/db/migrations/00051_messagetypeemail_report_notification.sql @@ -0,0 +1,3 @@ +-- +goose Up +ALTER TYPE comms.MessageTypeEmail ADD VALUE 'report-notification-confirmation' AFTER 'report-status-complete'; +UPDATE comms.email_log SET source = 'report-notification-confirmation' WHERE source = 'report-subscription-confirmation'; From 9d7ca815085745dac4b7dc257c3687d6a46fd475 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 2 Feb 2026 19:34:37 +0000 Subject: [PATCH 0249/1513] Make 'view in browser' on emails work correctly --- comms/email/db.go | 16 +++- comms/email/initial.go | 2 +- .../email/report_notification_confirmation.go | 2 +- comms/email/template.go | 15 ++++ db/dbinfo/comms.email_log.bob.go | 4 +- db/factory/bobfactory_main.bob.go | 2 +- db/factory/comms.email_log.bob.go | 79 +++++++------------ db/factory/comms.email_template.bob.go | 2 +- .../00052_comms_email_log_notnulls.sql | 2 + db/models/comms.email_log.bob.go | 29 +++---- db/models/comms.email_template.bob.go | 9 +-- rmo/email.go | 27 +++++-- rmo/routes.go | 2 +- 13 files changed, 106 insertions(+), 85 deletions(-) create mode 100644 db/migrations/00052_comms_email_log_notnulls.sql diff --git a/comms/email/db.go b/comms/email/db.go index d12e9316..c571ade2 100644 --- a/comms/email/db.go +++ b/comms/email/db.go @@ -27,6 +27,20 @@ func convertToPGData(data map[string]string) pgtypes.HStore { return result } +func convertFromPGData(d pgtypes.HStore) map[string]string { + result := make(map[string]string, 0) + for k, v := range d { + var s string + err := v.Scan(&s) + if err != nil { + log.Warn().Str("key", k).Msg("Failed to convert from HSTORE") + continue + } + result[k] = s + } + return result +} + func ensureInDB(ctx context.Context, destination string) (err error) { _, err = models.FindCommsEmailContact(ctx, db.PGInstance.BobDB, destination) if err != nil { @@ -61,7 +75,7 @@ func insertEmailLog(ctx context.Context, data map[string]string, destination str SentAt: omitnull.FromPtr[time.Time](nil), Source: omit.From(source), Subject: omit.From(subject), - TemplateID: omitnull.From(templateInitialID), + TemplateID: omit.From(templateInitialID), TemplateData: omit.From(data_for_insert), Type: omit.From(enums.CommsMessagetypeemailInitialContact), }).One(ctx, db.PGInstance.BobDB) diff --git a/comms/email/initial.go b/comms/email/initial.go index fd42fa1d..dbf92192 100644 --- a/comms/email/initial.go +++ b/comms/email/initial.go @@ -46,7 +46,7 @@ func sendEmailInitialContact(ctx context.Context, destination string) error { data["url_subscribe"] = config.MakeURLReport("/email/subscribe?email=%s", destination) data["url_unsubscribe"] = config.MakeURLReport("/email/unsubscribe") public_id := generatePublicId(enums.CommsMessagetypeemailInitialContact, data) - data["url_browser"] = config.MakeURLReport("/email?id=%s", public_id) + data["url_browser"] = config.MakeURLReport("/email/%s", public_id) text, html, err := renderEmailTemplates(templateInitialID, data) if err != nil { diff --git a/comms/email/report_notification_confirmation.go b/comms/email/report_notification_confirmation.go index efefd502..756a37bc 100644 --- a/comms/email/report_notification_confirmation.go +++ b/comms/email/report_notification_confirmation.go @@ -55,7 +55,7 @@ func sendEmailReportConfirmation(ctx context.Context, job Job) error { data["URLLogo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png") data["URLReportStatus"] = config.MakeURLReport("/foo") data["URLReportUnsubscribe"] = config.MakeURLReport("/email/unsubscribe") - data["URLViewInBrowser"] = config.MakeURLReport("/email?id=%s", public_id) + data["URLViewInBrowser"] = config.MakeURLReport("/email/%s", public_id) text, html, err := renderEmailTemplates(templateReportNotificationConfirmationID, data) if err != nil { return fmt.Errorf("Failed to render email report notification template: %w", err) diff --git a/comms/email/template.go b/comms/email/template.go index a59bbfe4..c3ad1a2c 100644 --- a/comms/email/template.go +++ b/comms/email/template.go @@ -19,6 +19,7 @@ import ( "github.com/Gleipnir-Technology/bob" "github.com/Gleipnir-Technology/bob/dialect/psql" "github.com/Gleipnir-Technology/bob/dialect/psql/um" + "github.com/Gleipnir-Technology/bob/types/pgtypes" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/enums" "github.com/Gleipnir-Technology/nidus-sync/db/models" @@ -90,6 +91,20 @@ func LoadTemplates() error { return nil } +func RenderHTML(template_id int32, s pgtypes.HStore) (html []byte, err error) { + data := convertFromPGData(s) + t, ok := templateByID[template_id] + if !ok { + return []byte{}, fmt.Errorf("Failed to lookup template %d", template_id) + } + buf_html := &bytes.Buffer{} + err = t.executeTemplateHTML(buf_html, data) + if err != nil { + return []byte{}, fmt.Errorf("Failed to render HTML template: %w", err) + } + return buf_html.Bytes(), nil +} + func loadTemplateID(ctx context.Context, tx bob.Tx, t enums.CommsMessagetypeemail) (int32, error) { templates, err := models.CommsEmailTemplates.Query( models.SelectWhere.CommsEmailTemplates.MessageType.EQ(t), diff --git a/db/dbinfo/comms.email_log.bob.go b/db/dbinfo/comms.email_log.bob.go index 09417e10..0cb69830 100644 --- a/db/dbinfo/comms.email_log.bob.go +++ b/db/dbinfo/comms.email_log.bob.go @@ -90,9 +90,9 @@ var CommsEmailLogs = Table[ TemplateID: column{ Name: "template_id", DBType: "integer", - Default: "NULL", + Default: "", Comment: "", - Nullable: true, + Nullable: false, Generated: false, AutoIncr: false, }, diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index fe532561..1d9ca8e2 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -228,7 +228,7 @@ func (f *Factory) FromExistingCommsEmailLog(m *models.CommsEmailLog) *CommsEmail o.SentAt = func() null.Val[time.Time] { return m.SentAt } o.Source = func() string { return m.Source } o.Subject = func() string { return m.Subject } - o.TemplateID = func() null.Val[int32] { return m.TemplateID } + o.TemplateID = func() int32 { return m.TemplateID } o.TemplateData = func() pgtypes.HStore { return m.TemplateData } o.Type = func() enums.CommsMessagetypeemail { return m.Type } diff --git a/db/factory/comms.email_log.bob.go b/db/factory/comms.email_log.bob.go index 04e52de6..e5d70db7 100644 --- a/db/factory/comms.email_log.bob.go +++ b/db/factory/comms.email_log.bob.go @@ -47,7 +47,7 @@ type CommsEmailLogTemplate struct { SentAt func() null.Val[time.Time] Source func() string Subject func() string - TemplateID func() null.Val[int32] + TemplateID func() int32 TemplateData func() pgtypes.HStore Type func() enums.CommsMessagetypeemail @@ -89,7 +89,7 @@ func (t CommsEmailLogTemplate) setModelRels(o *models.CommsEmailLog) { if t.r.TemplateEmailTemplate != nil { rel := t.r.TemplateEmailTemplate.o.Build() rel.R.TemplateEmailLogs = append(rel.R.TemplateEmailLogs, o) - o.TemplateID = null.From(rel.ID) // h2 + o.TemplateID = rel.ID // h2 o.R.TemplateEmailTemplate = rel } } @@ -133,7 +133,7 @@ func (o CommsEmailLogTemplate) BuildSetter() *models.CommsEmailLogSetter { } if o.TemplateID != nil { val := o.TemplateID() - m.TemplateID = omitnull.FromNull(val) + m.TemplateID = omit.From(val) } if o.TemplateData != nil { val := o.TemplateData() @@ -242,6 +242,10 @@ func ensureCreatableCommsEmailLog(m *models.CommsEmailLogSetter) { val := random_string(nil, "255") m.Subject = omit.From(val) } + if !(m.TemplateID.IsValue()) { + val := random_int32(nil) + m.TemplateID = omit.From(val) + } if !(m.TemplateData.IsValue()) { val := random_pgtypes_HStore(nil) m.TemplateData = omit.From(val) @@ -258,25 +262,6 @@ func ensureCreatableCommsEmailLog(m *models.CommsEmailLogSetter) { func (o *CommsEmailLogTemplate) insertOptRels(ctx context.Context, exec bob.Executor, m *models.CommsEmailLog) error { var err error - isTemplateEmailTemplateDone, _ := commsEmailLogRelTemplateEmailTemplateCtx.Value(ctx) - if !isTemplateEmailTemplateDone && o.r.TemplateEmailTemplate != nil { - ctx = commsEmailLogRelTemplateEmailTemplateCtx.WithValue(ctx, true) - if o.r.TemplateEmailTemplate.o.alreadyPersisted { - m.R.TemplateEmailTemplate = o.r.TemplateEmailTemplate.o.Build() - } else { - var rel1 *models.CommsEmailTemplate - rel1, err = o.r.TemplateEmailTemplate.o.Create(ctx, exec) - if err != nil { - return err - } - err = m.AttachTemplateEmailTemplate(ctx, exec, rel1) - if err != nil { - return err - } - } - - } - return err } @@ -304,12 +289,30 @@ func (o *CommsEmailLogTemplate) Create(ctx context.Context, exec bob.Executor) ( opt.Destination = omit.From(rel0.Address) + if o.r.TemplateEmailTemplate == nil { + CommsEmailLogMods.WithNewTemplateEmailTemplate().Apply(ctx, o) + } + + var rel1 *models.CommsEmailTemplate + + if o.r.TemplateEmailTemplate.o.alreadyPersisted { + rel1 = o.r.TemplateEmailTemplate.o.Build() + } else { + rel1, err = o.r.TemplateEmailTemplate.o.Create(ctx, exec) + if err != nil { + return nil, err + } + } + + opt.TemplateID = omit.From(rel1.ID) + m, err := models.CommsEmailLogs.Insert(opt).One(ctx, exec) if err != nil { return nil, err } m.R.DestinationEmailContact = rel0 + m.R.TemplateEmailTemplate = rel1 if err := o.insertOptRels(ctx, exec, m); err != nil { return nil, err @@ -673,14 +676,14 @@ func (m commsEmailLogMods) RandomSubject(f *faker.Faker) CommsEmailLogMod { } // Set the model columns to this value -func (m commsEmailLogMods) TemplateID(val null.Val[int32]) CommsEmailLogMod { +func (m commsEmailLogMods) TemplateID(val int32) CommsEmailLogMod { return CommsEmailLogModFunc(func(_ context.Context, o *CommsEmailLogTemplate) { - o.TemplateID = func() null.Val[int32] { return val } + o.TemplateID = func() int32 { return val } }) } // Set the Column from the function -func (m commsEmailLogMods) TemplateIDFunc(f func() null.Val[int32]) CommsEmailLogMod { +func (m commsEmailLogMods) TemplateIDFunc(f func() int32) CommsEmailLogMod { return CommsEmailLogModFunc(func(_ context.Context, o *CommsEmailLogTemplate) { o.TemplateID = f }) @@ -695,32 +698,10 @@ func (m commsEmailLogMods) UnsetTemplateID() CommsEmailLogMod { // Generates a random value for the column using the given faker // if faker is nil, a default faker is used -// The generated value is sometimes null func (m commsEmailLogMods) RandomTemplateID(f *faker.Faker) CommsEmailLogMod { return CommsEmailLogModFunc(func(_ context.Context, o *CommsEmailLogTemplate) { - o.TemplateID = func() null.Val[int32] { - if f == nil { - f = &defaultFaker - } - - val := random_int32(f) - return null.From(val) - } - }) -} - -// Generates a random value for the column using the given faker -// if faker is nil, a default faker is used -// The generated value is never null -func (m commsEmailLogMods) RandomTemplateIDNotNull(f *faker.Faker) CommsEmailLogMod { - return CommsEmailLogModFunc(func(_ context.Context, o *CommsEmailLogTemplate) { - o.TemplateID = func() null.Val[int32] { - if f == nil { - f = &defaultFaker - } - - val := random_int32(f) - return null.From(val) + o.TemplateID = func() int32 { + return random_int32(f) } }) } diff --git a/db/factory/comms.email_template.bob.go b/db/factory/comms.email_template.bob.go index a40c2f68..82774dda 100644 --- a/db/factory/comms.email_template.bob.go +++ b/db/factory/comms.email_template.bob.go @@ -77,7 +77,7 @@ func (t CommsEmailTemplateTemplate) setModelRels(o *models.CommsEmailTemplate) { for _, r := range t.r.TemplateEmailLogs { related := r.o.BuildMany(r.number) for _, rel := range related { - rel.TemplateID = null.From(o.ID) // h2 + rel.TemplateID = o.ID // h2 rel.R.TemplateEmailTemplate = o } rel = append(rel, related...) diff --git a/db/migrations/00052_comms_email_log_notnulls.sql b/db/migrations/00052_comms_email_log_notnulls.sql new file mode 100644 index 00000000..4e49e957 --- /dev/null +++ b/db/migrations/00052_comms_email_log_notnulls.sql @@ -0,0 +1,2 @@ +-- +goose Up +ALTER TABLE comms.email_log ALTER COLUMN template_id SET NOT NULL; diff --git a/db/models/comms.email_log.bob.go b/db/models/comms.email_log.bob.go index d22a9269..6c39c8cd 100644 --- a/db/models/comms.email_log.bob.go +++ b/db/models/comms.email_log.bob.go @@ -35,7 +35,7 @@ type CommsEmailLog struct { SentAt null.Val[time.Time] `db:"sent_at" ` Source string `db:"source" ` Subject string `db:"subject" ` - TemplateID null.Val[int32] `db:"template_id" ` + TemplateID int32 `db:"template_id" ` TemplateData pgtypes.HStore `db:"template_data" ` Type enums.CommsMessagetypeemail `db:"type" ` @@ -114,7 +114,7 @@ type CommsEmailLogSetter struct { SentAt omitnull.Val[time.Time] `db:"sent_at" ` Source omit.Val[string] `db:"source" ` Subject omit.Val[string] `db:"subject" ` - TemplateID omitnull.Val[int32] `db:"template_id" ` + TemplateID omit.Val[int32] `db:"template_id" ` TemplateData omit.Val[pgtypes.HStore] `db:"template_data" ` Type omit.Val[enums.CommsMessagetypeemail] `db:"type" ` } @@ -145,7 +145,7 @@ func (s CommsEmailLogSetter) SetColumns() []string { if s.Subject.IsValue() { vals = append(vals, "subject") } - if !s.TemplateID.IsUnset() { + if s.TemplateID.IsValue() { vals = append(vals, "template_id") } if s.TemplateData.IsValue() { @@ -182,8 +182,8 @@ func (s CommsEmailLogSetter) Overwrite(t *CommsEmailLog) { if s.Subject.IsValue() { t.Subject = s.Subject.MustGet() } - if !s.TemplateID.IsUnset() { - t.TemplateID = s.TemplateID.MustGetNull() + if s.TemplateID.IsValue() { + t.TemplateID = s.TemplateID.MustGet() } if s.TemplateData.IsValue() { t.TemplateData = s.TemplateData.MustGet() @@ -248,8 +248,8 @@ func (s *CommsEmailLogSetter) Apply(q *dialect.InsertQuery) { vals[7] = psql.Raw("DEFAULT") } - if !s.TemplateID.IsUnset() { - vals[8] = psql.Arg(s.TemplateID.MustGetNull()) + if s.TemplateID.IsValue() { + vals[8] = psql.Arg(s.TemplateID.MustGet()) } else { vals[8] = psql.Raw("DEFAULT") } @@ -333,7 +333,7 @@ func (s CommsEmailLogSetter) Expressions(prefix ...string) []bob.Expression { }}) } - if !s.TemplateID.IsUnset() { + if s.TemplateID.IsValue() { exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{ psql.Quote(append(prefix, "template_id")...), psql.Arg(s.TemplateID), @@ -612,7 +612,7 @@ func (o *CommsEmailLog) TemplateEmailTemplate(mods ...bob.Mod[*dialect.SelectQue } func (os CommsEmailLogSlice) TemplateEmailTemplate(mods ...bob.Mod[*dialect.SelectQuery]) CommsEmailTemplatesQuery { - pkTemplateID := make(pgtypes.Array[null.Val[int32]], 0, len(os)) + pkTemplateID := make(pgtypes.Array[int32], 0, len(os)) for _, o := range os { if o == nil { continue @@ -678,7 +678,7 @@ func (commsEmailLog0 *CommsEmailLog) AttachDestinationEmailContact(ctx context.C func attachCommsEmailLogTemplateEmailTemplate0(ctx context.Context, exec bob.Executor, count int, commsEmailLog0 *CommsEmailLog, commsEmailTemplate1 *CommsEmailTemplate) (*CommsEmailLog, error) { setter := &CommsEmailLogSetter{ - TemplateID: omitnull.From(commsEmailTemplate1.ID), + TemplateID: omit.From(commsEmailTemplate1.ID), } err := commsEmailLog0.Update(ctx, exec, setter) @@ -733,7 +733,7 @@ type commsEmailLogWhere[Q psql.Filterable] struct { SentAt psql.WhereNullMod[Q, time.Time] Source psql.WhereMod[Q, string] Subject psql.WhereMod[Q, string] - TemplateID psql.WhereNullMod[Q, int32] + TemplateID psql.WhereMod[Q, int32] TemplateData psql.WhereMod[Q, pgtypes.HStore] Type psql.WhereMod[Q, enums.CommsMessagetypeemail] } @@ -752,7 +752,7 @@ func buildCommsEmailLogWhere[Q psql.Filterable](cols commsEmailLogColumns) comms SentAt: psql.WhereNull[Q, time.Time](cols.SentAt), Source: psql.Where[Q, string](cols.Source), Subject: psql.Where[Q, string](cols.Subject), - TemplateID: psql.WhereNull[Q, int32](cols.TemplateID), + TemplateID: psql.Where[Q, int32](cols.TemplateID), TemplateData: psql.Where[Q, pgtypes.HStore](cols.TemplateData), Type: psql.Where[Q, enums.CommsMessagetypeemail](cols.Type), } @@ -947,11 +947,8 @@ func (os CommsEmailLogSlice) LoadTemplateEmailTemplate(ctx context.Context, exec } for _, rel := range commsEmailTemplates { - if !o.TemplateID.IsValue() { - continue - } - if !(o.TemplateID.IsValue() && o.TemplateID.MustGet() == rel.ID) { + if !(o.TemplateID == rel.ID) { continue } diff --git a/db/models/comms.email_template.bob.go b/db/models/comms.email_template.bob.go index 0f62168d..83471826 100644 --- a/db/models/comms.email_template.bob.go +++ b/db/models/comms.email_template.bob.go @@ -538,7 +538,7 @@ func (os CommsEmailTemplateSlice) TemplateEmailLogs(mods ...bob.Mod[*dialect.Sel func insertCommsEmailTemplateTemplateEmailLogs0(ctx context.Context, exec bob.Executor, commsEmailLogs1 []*CommsEmailLogSetter, commsEmailTemplate0 *CommsEmailTemplate) (CommsEmailLogSlice, error) { for i := range commsEmailLogs1 { - commsEmailLogs1[i].TemplateID = omitnull.From(commsEmailTemplate0.ID) + commsEmailLogs1[i].TemplateID = omit.From(commsEmailTemplate0.ID) } ret, err := CommsEmailLogs.Insert(bob.ToMods(commsEmailLogs1...)).All(ctx, exec) @@ -551,7 +551,7 @@ func insertCommsEmailTemplateTemplateEmailLogs0(ctx context.Context, exec bob.Ex func attachCommsEmailTemplateTemplateEmailLogs0(ctx context.Context, exec bob.Executor, count int, commsEmailLogs1 CommsEmailLogSlice, commsEmailTemplate0 *CommsEmailTemplate) (CommsEmailLogSlice, error) { setter := &CommsEmailLogSetter{ - TemplateID: omitnull.From(commsEmailTemplate0.ID), + TemplateID: omit.From(commsEmailTemplate0.ID), } err := commsEmailLogs1.UpdateAll(ctx, exec, *setter) @@ -730,10 +730,7 @@ func (os CommsEmailTemplateSlice) LoadTemplateEmailLogs(ctx context.Context, exe for _, rel := range commsEmailLogs { - if !rel.TemplateID.IsValue() { - continue - } - if !(rel.TemplateID.IsValue() && o.ID == rel.TemplateID.MustGet()) { + if !(o.ID == rel.TemplateID) { continue } diff --git a/rmo/email.go b/rmo/email.go index ebab6503..4a0e8106 100644 --- a/rmo/email.go +++ b/rmo/email.go @@ -1,18 +1,33 @@ package rmo import ( - "fmt" "net/http" - //"github.com/Gleipnir-Technology/nidus-sync/comms/email" + "github.com/Gleipnir-Technology/nidus-sync/comms/email" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/go-chi/chi/v5" ) func getEmailByCode(w http.ResponseWriter, r *http.Request) { - code := chi.URLParam(r, "code") - if code == "" { - http.Error(w, "You must specify a code", http.StatusBadRequest) + id := chi.URLParam(r, "code") + //id := r.FormValue("id") + if id == "" { + http.Error(w, "You must specify an id", http.StatusBadRequest) return } - fmt.Fprintf(w, "Pretend email contet for %s", code) + ctx := r.Context() + email_log, err := models.CommsEmailLogs.Query( + models.SelectWhere.CommsEmailLogs.PublicID.EQ(id), + ).One(ctx, db.PGInstance.BobDB) + if err != nil { + respondError(w, "Failed to query email_log: %w", err, http.StatusInternalServerError) + return + } + html, err := email.RenderHTML(email_log.TemplateID, email_log.TemplateData) + if err != nil { + respondError(w, "Failed to render email_log: %w", err, http.StatusInternalServerError) + return + } + w.Write(html) } diff --git a/rmo/routes.go b/rmo/routes.go index cace372c..f3028dad 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -24,7 +24,7 @@ func Router() chi.Router { r.Get("/privacy", getPrivacy) r.Get("/robots.txt", getRobots) - r.Get("/email", getEmailByCode) + r.Get("/email/{code}", getEmailByCode) r.Get("/image/{uuid}", getImageByUUID) r.Route("/mock", addMockRoutes) r.Get("/pool-submit-complete", getPoolSubmitComplete) From 4f56915f1545fd5b560b576768d2b54c962b5a1d Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 2 Feb 2026 19:54:32 +0000 Subject: [PATCH 0250/1513] Show a page for subscribing to emails. --- comms/email/initial.go | 17 ++++++++------ .../email/report_notification_confirmation.go | 2 +- comms/email/template/initial-contact.html | 10 +++++---- rmo/email.go | 19 ++++++++++++++++ rmo/routes.go | 3 ++- rmo/template/email-subscribe.html | 22 +++++++++++++++++++ 6 files changed, 60 insertions(+), 13 deletions(-) create mode 100644 rmo/template/email-subscribe.html diff --git a/comms/email/initial.go b/comms/email/initial.go index dbf92192..eb60a625 100644 --- a/comms/email/initial.go +++ b/comms/email/initial.go @@ -36,17 +36,20 @@ func maybeSendInitialEmail(ctx context.Context, destination string) error { return sendEmailInitialContact(ctx, destination) } +func urlEmailInBrowser(public_id string) string { + return config.MakeURLReport("/email/render/%s", public_id) +} func sendEmailInitialContact(ctx context.Context, destination string) error { //data := pgtypes.HStore{} data := make(map[string]string, 0) - source := config.ForwardEmailReportAddress - data["destination"] = destination - data["source"] = source - data["url_logo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png") - data["url_subscribe"] = config.MakeURLReport("/email/subscribe?email=%s", destination) - data["url_unsubscribe"] = config.MakeURLReport("/email/unsubscribe") public_id := generatePublicId(enums.CommsMessagetypeemailInitialContact, data) - data["url_browser"] = config.MakeURLReport("/email/%s", public_id) + source := config.ForwardEmailReportAddress + data["Destination"] = destination + data["Source"] = source + data["URLBrowser"] = urlEmailInBrowser(public_id) + data["URLLogo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png") + data["URLSubscribe"] = config.MakeURLReport("/email/subscribe?email=%s", destination) + data["URLUnsubscribe"] = config.MakeURLReport("/email/unsubscribe") text, html, err := renderEmailTemplates(templateInitialID, data) if err != nil { diff --git a/comms/email/report_notification_confirmation.go b/comms/email/report_notification_confirmation.go index 756a37bc..7aa23501 100644 --- a/comms/email/report_notification_confirmation.go +++ b/comms/email/report_notification_confirmation.go @@ -55,7 +55,7 @@ func sendEmailReportConfirmation(ctx context.Context, job Job) error { data["URLLogo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png") data["URLReportStatus"] = config.MakeURLReport("/foo") data["URLReportUnsubscribe"] = config.MakeURLReport("/email/unsubscribe") - data["URLViewInBrowser"] = config.MakeURLReport("/email/%s", public_id) + data["URLViewInBrowser"] = urlEmailInBrowser(public_id) text, html, err := renderEmailTemplates(templateReportNotificationConfirmationID, data) if err != nil { return fmt.Errorf("Failed to render email report notification template: %w", err) diff --git a/comms/email/template/initial-contact.html b/comms/email/template/initial-contact.html index 341e3ae4..f30b6960 100644 --- a/comms/email/template/initial-contact.html +++ b/comms/email/template/initial-contact.html @@ -64,32 +64,34 @@
+ {{if .IsBrowser}}
Email not displaying correctly? View it in your browser
+ {{end}}
- +

Welcome

-

We're sending you this email because it's the first time we've gotten this email address ({{.destination}}).

+

We're sending you this email because it's the first time we've gotten this email address ({{.Destination}}).

If you'd rather not receive emails from us you can reply with "Unsubscribe" in the subject or body of the email. You can also use the "Unsubscribe" feature of your mail client, if it supports list unsubscribes.

If instead you'd like to confirm that you're willing to receive emails at this address, you can do so by clicking below:

diff --git a/rmo/email.go b/rmo/email.go index 4a0e8106..df3db1cd 100644 --- a/rmo/email.go +++ b/rmo/email.go @@ -6,9 +6,18 @@ import ( "github.com/Gleipnir-Technology/nidus-sync/comms/email" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" + "github.com/Gleipnir-Technology/nidus-sync/html" "github.com/go-chi/chi/v5" ) +type ContentEmailSubscribe struct { + Email string +} + +var ( + EmailSubscribeT = buildTemplate("email-subscribe", "base") +) + func getEmailByCode(w http.ResponseWriter, r *http.Request) { id := chi.URLParam(r, "code") //id := r.FormValue("id") @@ -31,3 +40,13 @@ func getEmailByCode(w http.ResponseWriter, r *http.Request) { } w.Write(html) } +func getEmailSubscribe(w http.ResponseWriter, r *http.Request) { + email := r.FormValue("email") + html.RenderOrError( + w, + EmailSubscribeT, + ContentEmailSubscribe{ + Email: email, + }, + ) +} diff --git a/rmo/routes.go b/rmo/routes.go index f3028dad..ad664929 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -24,7 +24,8 @@ func Router() chi.Router { r.Get("/privacy", getPrivacy) r.Get("/robots.txt", getRobots) - r.Get("/email/{code}", getEmailByCode) + r.Get("/email/render/{code}", getEmailByCode) + r.Get("/email/subscribe", getEmailSubscribe) r.Get("/image/{uuid}", getImageByUUID) r.Route("/mock", addMockRoutes) r.Get("/pool-submit-complete", getPoolSubmitComplete) diff --git a/rmo/template/email-subscribe.html b/rmo/template/email-subscribe.html new file mode 100644 index 00000000..fa7469a7 --- /dev/null +++ b/rmo/template/email-subscribe.html @@ -0,0 +1,22 @@ +{{template "base.html" .}} + +{{define "title"}}Main{{end}} +{{define "extraheader"}} +{{end}} +{{define "content"}} + +
+
+ +
+
+
+

Thanks!

+

You've allowed emails from Report Mosquitoes Online to {{.Email}}.

+
+
+
+ +{{end}} From a52f5da87a314c1832249a01c4638099fabcf600 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 2 Feb 2026 20:01:28 +0000 Subject: [PATCH 0251/1513] Mark email addresses confirmed when they click the big button. --- rmo/email.go | 14 ++++++++++++++ rmo/template/email-subscribe.html | 2 ++ 2 files changed, 16 insertions(+) diff --git a/rmo/email.go b/rmo/email.go index df3db1cd..0a9422f2 100644 --- a/rmo/email.go +++ b/rmo/email.go @@ -7,6 +7,7 @@ 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/aarondl/opt/omit" "github.com/go-chi/chi/v5" ) @@ -42,6 +43,19 @@ func getEmailByCode(w http.ResponseWriter, r *http.Request) { } func getEmailSubscribe(w http.ResponseWriter, r *http.Request) { email := r.FormValue("email") + if email == "" { + respondError(w, "Not sure what to do with an empty email", nil, http.StatusBadRequest) + return + } + ctx := r.Context() + email_contact, err := models.FindCommsEmailContact(ctx, db.PGInstance.BobDB, email) + if err != nil { + respondError(w, "Email not in the database", err, http.StatusNotFound) + return + } + err = email_contact.Update(ctx, db.PGInstance.BobDB, &models.CommsEmailContactSetter{ + Confirmed: omit.From(true), + }) html.RenderOrError( w, EmailSubscribeT, diff --git a/rmo/template/email-subscribe.html b/rmo/template/email-subscribe.html index fa7469a7..d88a5b08 100644 --- a/rmo/template/email-subscribe.html +++ b/rmo/template/email-subscribe.html @@ -15,6 +15,8 @@

Thanks!

You've allowed emails from Report Mosquitoes Online to {{.Email}}.

+

Go ahead and close this page/tab/window whenever you're ready...

+

...or maybe check out the site

From b2737b49684918436e0cad54fdd6c86030f479cf Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Mon, 2 Feb 2026 21:34:36 +0000 Subject: [PATCH 0252/1513] Add routes for confirming email address --- comms/email/initial.go | 5 +- .../email/report_notification_confirmation.go | 3 +- .../report-notification-confirmation.html | 1 + rmo/email.go | 71 +++++++++++++++++-- rmo/routes.go | 6 +- rmo/template/email-confirm-complete.html | 24 +++++++ rmo/template/email-confirm.html | 29 ++++++++ rmo/template/email-subscribe.html | 12 ++-- 8 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 rmo/template/email-confirm-complete.html create mode 100644 rmo/template/email-confirm.html diff --git a/comms/email/initial.go b/comms/email/initial.go index eb60a625..bfc96d29 100644 --- a/comms/email/initial.go +++ b/comms/email/initial.go @@ -39,6 +39,9 @@ func maybeSendInitialEmail(ctx context.Context, destination string) error { func urlEmailInBrowser(public_id string) string { return config.MakeURLReport("/email/render/%s", public_id) } +func urlUnsubscribe(address string) string { + return config.MakeURLReport("/email/unsubscribe?email=%s") +} func sendEmailInitialContact(ctx context.Context, destination string) error { //data := pgtypes.HStore{} data := make(map[string]string, 0) @@ -49,7 +52,7 @@ func sendEmailInitialContact(ctx context.Context, destination string) error { data["URLBrowser"] = urlEmailInBrowser(public_id) data["URLLogo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png") data["URLSubscribe"] = config.MakeURLReport("/email/subscribe?email=%s", destination) - data["URLUnsubscribe"] = config.MakeURLReport("/email/unsubscribe") + data["URLUnsubscribe"] = urlUnsubscribe(destination) text, html, err := renderEmailTemplates(templateInitialID, data) if err != nil { diff --git a/comms/email/report_notification_confirmation.go b/comms/email/report_notification_confirmation.go index 7aa23501..5c9871e0 100644 --- a/comms/email/report_notification_confirmation.go +++ b/comms/email/report_notification_confirmation.go @@ -54,7 +54,8 @@ func sendEmailReportConfirmation(ctx context.Context, job Job) error { data["ReportIDStr"] = report_id_str data["URLLogo"] = config.MakeURLReport("/static/img/nidus-logo-no-lettering-64.png") data["URLReportStatus"] = config.MakeURLReport("/foo") - data["URLReportUnsubscribe"] = config.MakeURLReport("/email/unsubscribe") + data["URLReportUnsubscribe"] = config.MakeURLReport("/email/unsubscribe/report/%s", j.reportID) + data["URLUnsubscribe"] = urlUnsubscribe(j.destination()) data["URLViewInBrowser"] = urlEmailInBrowser(public_id) text, html, err := renderEmailTemplates(templateReportNotificationConfirmationID, data) if err != nil { diff --git a/comms/email/template/report-notification-confirmation.html b/comms/email/template/report-notification-confirmation.html index 0eef1e71..2ae0db35 100644 --- a/comms/email/template/report-notification-confirmation.html +++ b/comms/email/template/report-notification-confirmation.html @@ -87,6 +87,7 @@

We'll send you additional updates as work is scheduled and completed.

If you have any questions or need further assistance, please don't hesitate to contact us by replying to this email.

+

You can unsubscribe from notifications about this report by clicking here

+
{{end}} diff --git a/scss/custom.scss b/scss/custom.scss index 3d4714c5..b9ded3a1 100644 --- a/scss/custom.scss +++ b/scss/custom.scss @@ -43,3 +43,4 @@ $theme-colors: map-merge( @import "./sidebar.scss"; @import "./rmo/nuisance.scss"; @import "./rmo/root.scss"; +@import "./rmo/status.scss"; diff --git a/scss/status.scss b/scss/status.scss new file mode 100644 index 00000000..cdf5161f --- /dev/null +++ b/scss/status.scss @@ -0,0 +1,30 @@ +.map-container { + background-color: #e9ecef; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0,0,0,0.05); + height: 500px; + display: flex; + align-items: center; + justify-content: center; + margin-top: 20px; +} +#map { + height: 500px; + width:100%; + margin-bottom: 10px; +} +#map img { + max-width: none; + min-width: 0px; + height: auto; +} +.search-box { + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + border-radius: 8px; +} +@media (max-width: 768px) { + .map-container { + height: 300px; + } +} + From a9e333b73a810504b9f23eb08bb17c97f50bceed Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 3 Feb 2026 22:11:54 +0000 Subject: [PATCH 0262/1513] Add new custom element for handling report ID or location Looks purty. --- .../static/js/address-or-report-suggestion.js | 209 ++++++++++++++++++ rmo/template/status.html | 43 +--- 2 files changed, 213 insertions(+), 39 deletions(-) create mode 100644 html/static/js/address-or-report-suggestion.js diff --git a/html/static/js/address-or-report-suggestion.js b/html/static/js/address-or-report-suggestion.js new file mode 100644 index 00000000..5b6ea627 --- /dev/null +++ b/html/static/js/address-or-report-suggestion.js @@ -0,0 +1,209 @@ +class AddressOrReportInput extends HTMLElement { + // make element form-associated + static formAssociated = true; + + constructor() { + super(); + + this.attachShadow({mode: "open" }); + this.internals = this.attachInternals(); + this.render(); + + // Element references + this._input = this.shadowRoot.querySelector("input"); + this._suggestions = this.shadowRoot.querySelector(".suggestions-container"); + + // Bind methods + this._handleInput = this._handleInput.bind(this); + + // Debounce timer + this._debounceTimer = null; + + // The suggestion data + this._suggestionData = null; + } + + // Lifecycle: when element is added to the DOM + connectedCallback() { + this._input.addEventListener("input", this._handleInput); + } + + // Lifecycle: when element is removed from the DOM + disconnectedCallback() { + this._input.removeEventListener('input', this._handleInput); + } + + // Lifecycle: watch these attributes for changes + static get observedAttributes() { + return ['placeholder', 'api-key']; + } + + // Lifecycle: respond to attribute changes + attributeChangedCallback(name, oldValue, newValue) { + if (name === 'placeholder' && this._input) { + this._input.placeholder = newValue; + } + + if (name === 'api-key') { + this._apiKey = newValue; + } + } + + // Properties API + get value() { + return this._input ? this._input.value : ''; + } + + set value(val) { + if (this._input) { + this._input.value = val; + const entries = new FormData(); + entries.append("address", val); + this.internals.setFormValue(entries); + } + } + + // Private methods + _handleInput(event) { + const searchText = event.target.value.trim(); + + // Clear previous timer + clearTimeout(this._debounceTimer); + + // Clear suggestions if input is less than 3 characters + if (searchText.length < 3) { + this._suggestions.innerHTML = ''; + return; + } + + // Debounce API calls (wait 300ms after typing stops) + this._debounceTimer = setTimeout(() => { + this._fetchAddressSuggestions(searchText) + .then(response => { + this._renderSuggestions(response.features); + }); + }, 300); + + } + + async _fetchAddressSuggestions(text) { + try { + const url = `https://api.mapbox.com/search/geocode/v6/forward?q=${encodeURIComponent(text)}&access_token=${this._apiKey}`; + + const response = await fetch(url); + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching geocoding suggestions:', error); + } + } + + _renderSuggestions(suggestions) { + console.log("Rendering suggestions", suggestions); + this._suggestions.innerHTML = suggestions.map((item, index) => { + if (item.properties.place_formatted != "") { + return ` +
+
${item.properties.name || item.properties.full_address}
+
${item.properties.place_formatted}
+
` + } else { + return ` +
+
${item.properties.name || item.properties.full_address}
+
${item.properties.place_formatted}
+
` + } + }).join(''); + + // Add click listeners to suggestions + this.shadowRoot.querySelectorAll('.suggestion-item').forEach(el => { + el.addEventListener('click', e => { + const index = parseInt(el.dataset.index); + const suggestion = suggestions[index]; + this.SetValue(suggestion); + // Dispatch custom event + this.dispatchEvent(new CustomEvent('address-selected', { + bubbles: true, + composed: true, // Allows event to cross shadow DOM boundary + detail: { + location: suggestion + } + })); + }); + }); + } + + // Initial render of component + render() { + const placeholder = this.getAttribute('placeholder') || 'Enter address'; + + this.shadowRoot.innerHTML = ` + + +
+ + +
+
+ `; + } + + // Public methods + clear() { + if (this._input) { + this._input.value = ''; + this._suggestions.innerHTML = ''; + } + } + + SetValue(suggestion) { + this.value = suggestion.properties.full_address; + this._suggestions.innerHTML = ''; + } +} + +customElements.define('address-or-report-input', AddressOrReportInput); diff --git a/rmo/template/status.html b/rmo/template/status.html index 5795681d..a8f3e800 100644 --- a/rmo/template/status.html +++ b/rmo/template/status.html @@ -3,6 +3,7 @@ {{define "title"}}Status{{end}} {{define "extraheader"}} + @@ -10,40 +11,7 @@ {{end}} @@ -54,12 +22,9 @@ document.addEventListener('DOMContentLoaded', function() {
- -
- - -
+
From 26223ccc0aba10fbd5947e6ae8d1d12f38b957d6 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 3 Feb 2026 23:11:19 +0000 Subject: [PATCH 0263/1513] Fix suggestion container offset --- html/static/js/address-or-report-suggestion.js | 1 + 1 file changed, 1 insertion(+) diff --git a/html/static/js/address-or-report-suggestion.js b/html/static/js/address-or-report-suggestion.js index 5b6ea627..7e8b995b 100644 --- a/html/static/js/address-or-report-suggestion.js +++ b/html/static/js/address-or-report-suggestion.js @@ -173,6 +173,7 @@ class AddressOrReportInput extends HTMLElement { overflow-y: auto; z-index: 1000; box-shadow: 0 4px 8px rgba(0,0,0,0.1); + top: 48px; } .suggestion-item { cursor: pointer; From fa0ac035ac33b16408a4c7ea624df53050221f6e Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 3 Feb 2026 23:12:40 +0000 Subject: [PATCH 0264/1513] Add scss debug request endpoint To help with debugging my scss. --- rmo/routes.go | 1 + rmo/scss.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 rmo/scss.go diff --git a/rmo/routes.go b/rmo/routes.go index 7cc8141b..ddb231ea 100644 --- a/rmo/routes.go +++ b/rmo/routes.go @@ -39,6 +39,7 @@ func Router() chi.Router { r.Post("/register-notifications", postRegisterNotifications) r.Get("/register-notifications-complete", getRegisterNotificationsComplete) r.Get("/search", getSearch) + r.Get("/scss/*", getScssDebug) r.Get("/status", getStatus) r.Get("/status/{report_id}", getStatusByID) r.Get("/terms-of-service", getTerms) diff --git a/rmo/scss.go b/rmo/scss.go new file mode 100644 index 00000000..e58c89c5 --- /dev/null +++ b/rmo/scss.go @@ -0,0 +1,39 @@ +package rmo + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/go-chi/chi/v5" + "github.com/rs/zerolog/log" +) + +func getScssDebug(w http.ResponseWriter, r *http.Request) { + path := chi.URLParam(r, "*") + full_path := "scss/" + path + //log.Debug().Str("path", path).Str("full_path", full_path).Msg("working on SCSS debug") + file, err := os.Open(full_path) + if err != nil { + respondError(w, "failed to open file", err, http.StatusInternalServerError) + return + } + defer file.Close() + fileInfo, err := file.Stat() + if err != nil { + respondError(w, "failed to stat file", err, http.StatusInternalServerError) + return + } + // Set appropriate headers + w.Header().Set("Content-Type", "text/scss") + w.Header().Set("Content-Length", fmt.Sprintf("%d", fileInfo.Size())) + // Copy file contents to response writer + _, err = io.Copy(w, file) + if err != nil { + // Note: At this point, we've already started writing the response, + // so we can't change the status code anymore. The best we can do + // is log the error and abandon the connection. + log.Warn().Str("path", path).Msg("Failed to write scss file to output") + } +} From 765e437d6c028da11be26b9b8295429dda1810d0 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Tue, 3 Feb 2026 23:12:59 +0000 Subject: [PATCH 0265/1513] Add header to status search page --- rmo/status.go | 1 + rmo/template/status.html | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/rmo/status.go b/rmo/status.go index 3eceb5b9..c5e875ac 100644 --- a/rmo/status.go +++ b/rmo/status.go @@ -31,6 +31,7 @@ type Contact struct { Phone string } type ContentStatus struct { + District *ContentDistrict Error string MapboxToken string ReportID string diff --git a/rmo/template/status.html b/rmo/template/status.html index a8f3e800..88fc4e0d 100644 --- a/rmo/template/status.html +++ b/rmo/template/status.html @@ -16,6 +16,11 @@ document.addEventListener('DOMContentLoaded', function() { {{end}} {{define "content"}} +{{if (eq .District nil)}} + {{template "header-rmo" .}} +{{else}} + {{template "header-district" .District}} +{{end}}
- - -
-
-
- - - - - Need Help? -
-

If you need to update your contact information or have questions about your report, please contact our Mosquito Control Unit at (123) 456-7890 or mosquito@example.gov and reference your Report ID.

-
-
From 63358e684887d74d1317b0e04491e0b2adc485cd Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 4 Feb 2026 16:26:30 +0000 Subject: [PATCH 0270/1513] Render nearby reports on the search map. --- rmo/template/status.html | 85 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/rmo/template/status.html b/rmo/template/status.html index 88fc4e0d..7772604a 100644 --- a/rmo/template/status.html +++ b/rmo/template/status.html @@ -11,8 +11,89 @@ {{end}} {{define "content"}} From 820f8237d13607474f25438dd310b1c39d720479 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Wed, 4 Feb 2026 17:12:46 +0000 Subject: [PATCH 0271/1513] Show pools and nuisances in separate layers on status search --- html/static/js/report-table.js | 6 +-- rmo/template/status.html | 67 ++++++++++++++++++++++------------ scss/rmo/status.scss | 36 ++++++++++++++++++ 3 files changed, 83 insertions(+), 26 deletions(-) diff --git a/html/static/js/report-table.js b/html/static/js/report-table.js index 78c8af22..a66e5f12 100644 --- a/html/static/js/report-table.js +++ b/html/static/js/report-table.js @@ -70,11 +70,11 @@ class ReportTable extends HTMLElement { */ getTypeClass(type) { switch(type) { - case 'Nuisance': + case 'nuisance': return 'bg-danger'; - case 'Quick': + case 'quick': return 'bg-primary'; - case 'Green Pool': + case 'pool': return 'bg-success'; default: return 'bg-secondary'; diff --git a/rmo/template/status.html b/rmo/template/status.html index 7772604a..72aac1a3 100644 --- a/rmo/template/status.html +++ b/rmo/template/status.html @@ -8,22 +8,31 @@ - {{end}} @@ -152,14 +172,16 @@ document.addEventListener('DOMContentLoaded', onLoad);