From a82732a49cc27d9ea293e1ab93da5839af82b696 Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Fri, 1 May 2026 20:49:37 +0000 Subject: [PATCH] Return communication database rows from communication API This is a pretty big refactor of how communication works to start moving us in the direction we want to go long-term. This adds the new communication row and migrates existing reports to add rows for communication. There's also a bunch of automatic fixes from the new linter. I should have added them separately, but whatever. --- api/event.go | 2 +- api/handler.go | 7 +- api/routes.go | 4 +- auth/auth.go | 2 +- cmd/passwordgen/main.go | 2 +- config/config.go | 2 +- db/connection.go | 4 - db/jet/main.go | 2 + db/migrations/00147_communication.sql | 1 + .../00148_communications_from_reports.sql | 37 ++++++++ db/migrations/00149_permissionaccesstype.sql | 22 +++++ db/prepared.go | 47 ---------- db/query/public/communication.go | 26 ++++++ db/query/publicreport/report.go | 32 +++++++ html/embed.go | 4 +- label-studio/tasks_draft.go | 2 +- llm/openai.go | 2 +- middleware/logger.go | 6 +- middleware/wrap_writer.go | 24 ++--- platform/arcgis.go | 8 +- platform/communication.go | 12 +++ platform/csv/pool.go | 4 +- platform/label_studio.go | 4 +- platform/publicreport.go | 16 ++++ platform/publicreport/log.go | 4 +- platform/text/text.go | 6 +- platform/trap.go | 5 +- resource/communication.go | 89 +++++++++++++----- resource/publicreport.go | 2 +- resource/query_params.go | 6 +- resource/upload.go | 2 +- resource/user.go | 2 +- tomtom/example/geocode-and-route/main.go | 4 +- tomtom/geocode.go | 5 +- ts/components/CommunicationColumnList.vue | 4 +- ts/components/ListCardCommunication.vue | 92 +++++++++++++++++++ ts/components/ListCardPublicReport.vue | 65 ------------- ts/format.ts | 4 +- ts/store/communication.ts | 8 +- ts/type/api.ts | 6 +- ts/view/Communication.vue | 9 +- 41 files changed, 365 insertions(+), 220 deletions(-) create mode 100644 db/migrations/00148_communications_from_reports.sql create mode 100644 db/migrations/00149_permissionaccesstype.sql create mode 100644 db/query/public/communication.go create mode 100644 db/query/publicreport/report.go create mode 100644 platform/communication.go create mode 100644 ts/components/ListCardCommunication.vue delete mode 100644 ts/components/ListCardPublicReport.vue diff --git a/api/event.go b/api/event.go index 1ba713e7..e5af4948 100644 --- a/api/event.go +++ b/api/event.go @@ -67,7 +67,7 @@ func (c *ConnectionSSE) SendHeartbeat(w http.ResponseWriter, t time.Time) error func SetEventChannel(chan_envelopes <-chan platform.Envelope) { go func() { for envelope := range chan_envelopes { - for conn, _ := range connectionsSSE { + for conn := range connectionsSSE { if conn.organizationID == envelope.OrganizationID || envelope.OrganizationID == 0 { log.Debug().Int("type", int(envelope.Event.Type)).Int32("env-org", envelope.OrganizationID).Msg("pushed event to client") conn.chanEvent <- envelope.Event diff --git a/api/handler.go b/api/handler.go index 88a47293..b44638c5 100644 --- a/api/handler.go +++ b/api/handler.go @@ -345,19 +345,20 @@ func parseRequest[RequestType any](r *http.Request) (*RequestType, *nhttp.ErrorW var err error var req RequestType content_type := r.Header.Get("Content-Type") - if content_type == "application/json" { + switch content_type { + case "application/json": body, e := io.ReadAll(r.Body) if e != nil { return nil, nhttp.NewError("Failed to read body: %w", err) } err = json.Unmarshal(body, &req) - } else if content_type == "application/x-www-form-urlencoded" { + case "application/x-www-form-urlencoded": e := r.ParseForm() if err != nil { return nil, nhttp.NewBadRequest("parsing form: %w", e) } err = decoder.Decode(&req, r.PostForm) - } else { + default: return nil, nhttp.NewBadRequest("unrecognized content type '%s'", content_type) } if err != nil { diff --git a/api/routes.go b/api/routes.go index 590b0b1f..47222d02 100644 --- a/api/routes.go +++ b/api/routes.go @@ -98,7 +98,7 @@ func AddRoutesSync(r *mux.Router) { r.Handle("/avatar", authenticatedHandlerPostMultipart(avatar.Create, file.CollectionAvatar)).Methods("POST") r.Handle("/client/ios", auth.NewEnsureAuth(handleClientIos)).Methods("GET") communication := resource.Communication(router) - r.Handle("/communication", authenticatedHandlerJSON(communication.List)).Methods("GET") + r.Handle("/communication", authenticatedHandlerJSONSlice(communication.List)).Methods("GET") r.Handle("/compliance-request/mailer", authenticatedHandlerJSONPost(compliance_request.CreateMailer)).Methods("POST") //r.HandleFunc("/compliance-request/image/pool/{public_id}", getComplianceRequestImagePool).Methods("GET") r.Handle("/configuration/integration/arcgis", authenticatedHandlerJSONPost(postConfigurationIntegrationArcgis)).Methods("POST") @@ -121,7 +121,7 @@ func AddRoutesSync(r *mux.Router) { r.Handle("/publicreport/invalid", authenticatedHandlerJSONPost(postPublicreportInvalid)).Methods("POST") r.Handle("/publicreport/signal", authenticatedHandlerJSONPost(postPublicreportSignal)).Methods("POST") r.Handle("/publicreport/message", authenticatedHandlerJSONPost(postPublicreportMessage)).Methods("POST") - r.Handle("/publicreport/{id}", authenticatedHandlerBasic(publicreport.ByID)).Methods("GET").Name("publicreport.ByIDGetPublic") + r.Handle("/publicreport/{id}", authenticatedHandlerBasic(publicreport.ByID)).Methods("GET").Name("publicreport.ByIDGet") r.Handle("/publicreport/compliance/{id}", authenticatedHandlerJSON(pr_compliance.ByID)).Methods("GET").Name("publicreport.compliance.ByIDGet") r.Handle("/publicreport/nuisance/{id}", authenticatedHandlerJSON(nuisance.ByID)).Methods("GET").Name("publicreport.nuisance.ByIDGet") r.Handle("/publicreport/water/{id}", authenticatedHandlerJSON(water.ByID)).Methods("GET").Name("publicreport.water.ByIDGet") diff --git a/auth/auth.go b/auth/auth.go index e19be03a..ae8e12ac 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -126,7 +126,7 @@ func (ea *EnsureAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Don't send authentication headers for browsers because it forces the authentication popup requested_with := r.Header.Get("X-Requested-With") //log.Debug().Str("x-requested-with", requested_with).Send() - if !(strings.HasPrefix(requested_with, "nidus-web") || accept == "text/event-stream") { + if !strings.HasPrefix(requested_with, "nidus-web") && accept != "text/event-stream" { w.Header().Set("WWW-Authenticate", `Basic realm="Nidus Sync"`) // Separate return codes for different authentication failures if _, ok := err.(*NoCredentialsError); ok { diff --git a/cmd/passwordgen/main.go b/cmd/passwordgen/main.go index e0b3aad4..4513efec 100644 --- a/cmd/passwordgen/main.go +++ b/cmd/passwordgen/main.go @@ -26,7 +26,7 @@ func main() { } func scanValue(message string, result *string) { - fmt.Printf(message) + fmt.Print("%s", message) scanner := bufio.NewScanner(os.Stdin) if ok := scanner.Scan(); !ok { log.Fatal(errors.New("Failed to scan input")) diff --git a/config/config.go b/config/config.go index ba1a7495..1408b516 100644 --- a/config/config.go +++ b/config/config.go @@ -98,7 +98,7 @@ func Parse() (err error) { if Environment == "" { return fmt.Errorf("You must specify a non-empty ENVIRONMENT") } - if !(Environment == "PRODUCTION" || Environment == "DEVELOPMENT") { + if Environment != "PRODUCTION" && Environment != "DEVELOPMENT" { return fmt.Errorf("ENVIRONMENT should be either DEVELOPMENT or PRODUCTION") } FieldseekerSchemaDirectory = os.Getenv("FIELDSEEKER_SCHEMA_DIRECTORY") diff --git a/db/connection.go b/db/connection.go index b8a72df9..fe84d47e 100644 --- a/db/connection.go +++ b/db/connection.go @@ -149,10 +149,6 @@ func InitializeDatabase(ctx context.Context, uri string) error { if err != nil { return fmt.Errorf("Failed to get database current: %w", err) } - err = prepareStatements(ctx) - if err != nil { - return fmt.Errorf("Failed to initialize prepared statements: %w", err) - } return nil } diff --git a/db/jet/main.go b/db/jet/main.go index efb987b2..5f52cb1f 100644 --- a/db/jet/main.go +++ b/db/jet/main.go @@ -15,6 +15,8 @@ import ( var schemas []string = []string{ "arcgis", + "public", + "publicreport", "stadia", } diff --git a/db/migrations/00147_communication.sql b/db/migrations/00147_communication.sql index 57577061..db994cb5 100644 --- a/db/migrations/00147_communication.sql +++ b/db/migrations/00147_communication.sql @@ -8,6 +8,7 @@ CREATE TABLE communication ( invalidated_by INTEGER REFERENCES user_(id), opened TIMESTAMP WITHOUT TIME ZONE, opened_by INTEGER REFERENCES user_(id), + organization_id INTEGER NOT NULL REFERENCES organization(id), response_email_log_id INTEGER REFERENCES comms.email_log(id), response_text_log_id INTEGER REFERENCES comms.text_log(id), set_pending TIMESTAMP WITHOUT TIME ZONE, diff --git a/db/migrations/00148_communications_from_reports.sql b/db/migrations/00148_communications_from_reports.sql new file mode 100644 index 00000000..21841263 --- /dev/null +++ b/db/migrations/00148_communications_from_reports.sql @@ -0,0 +1,37 @@ +-- +goose Up +INSERT INTO communication ( + closed, + closed_by, + created, + --id, + invalidated, + invalidated_by, + opened, + opened_by, + organization_id, + response_email_log_id, + response_text_log_id, + set_pending, + set_pending_by, + source_email_log_id, + source_report_id, + source_text_log_id +) SELECT + NULL, + NULL, + created, + NULL, + NULL, + NULL, + NULL, + organization_id, + NULL, + NULL, + NULL, + NULL, + NULL, + id, + NULL +FROM publicreport.report; +-- +goose Down +DELETE FROM communication; diff --git a/db/migrations/00149_permissionaccesstype.sql b/db/migrations/00149_permissionaccesstype.sql new file mode 100644 index 00000000..9d07d5d6 --- /dev/null +++ b/db/migrations/00149_permissionaccesstype.sql @@ -0,0 +1,22 @@ +-- +goose Up +CREATE TYPE publicreport.PermissionAccessType AS ENUM ( + 'denied', + 'granted', + 'unselected', + 'with-owner' +); +ALTER TABLE publicreport.compliance + ALTER COLUMN permission_type + TYPE publicreport.PermissionAccessType USING permission_type::text::publicreport.PermissionAccessType; +DROP TYPE PermissionAccessType; +-- +goose Down +CREATE TYPE PermissionAccessType AS ENUM ( + 'denied', + 'granted', + 'unselected', + 'with-owner' +); +ALTER TABLE publicreport.compliance + ALTER COLUMN permission_type + TYPE PermissionAccessType; +DROP TYPE publicreport.PermissionAccessType; diff --git a/db/prepared.go b/db/prepared.go index 33b34758..4cdc8e09 100644 --- a/db/prepared.go +++ b/db/prepared.go @@ -5,7 +5,6 @@ import ( "embed" "encoding/json" "fmt" - "path/filepath" "strings" "time" @@ -21,52 +20,6 @@ import ( //go:embed prepared_functions/*.sql var sqlFiles embed.FS -// PrepareStatements reads all embedded SQL files and executes them -// against the provided database connection. This is intended for -// preparing statements that will be used later. -func prepareStatements(ctx context.Context) error { - return nil - // Get a list of all embedded SQL files - entries, err := sqlFiles.ReadDir("prepared_functions") - if err != nil { - return fmt.Errorf("failed to read SQL directory: %w", err) - } - log.Info().Int("len", len(entries)).Msg("Reading prepared functions") - - // Process each SQL file - for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") { - log.Info().Str("name", entry.Name()).Msg("Skipping") - continue - } - - // Read the SQL file content - content, err := sqlFiles.ReadFile(filepath.Join("prepared_functions", entry.Name())) - if err != nil { - return fmt.Errorf("failed to read SQL file %s: %w", entry.Name(), err) - } - - // Get the statement name from the filename (without extension) - statementName := strings.TrimSuffix(filepath.Base(entry.Name()), ".sql") - - // Execute the SQL to prepare the statement - _, err = PGInstance.BobDB.Exec(string(content)) - if err != nil { - return fmt.Errorf("failed to prepare statement %s: %w", statementName, err) - } - /* - query := psql.RawQuery(string(content)) - stmt, err := bob.Prepare(ctx, PGInstance.BobDB, query) - if err != nil { - return fmt.Errorf("failed to prepare statement %s: %w", statementName, err) - } - */ - - log.Info().Str("statement", statementName).Msg("Prepared statement") - } - - return nil -} func TestPreparedQueryOld(ctx context.Context) error { type Skn struct { Result int diff --git a/db/query/public/communication.go b/db/query/public/communication.go new file mode 100644 index 00000000..52235a53 --- /dev/null +++ b/db/query/public/communication.go @@ -0,0 +1,26 @@ +package public + +import ( + "context" + "time" + + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/table" + "github.com/go-jet/jet/v2/postgres" +) + +func CommunicationInsert(ctx context.Context, txn bob.Tx, m *model.Communication) (*model.Communication, error) { + m.Created = time.Now() + statement := table.Communication.INSERT(table.Communication.MutableColumns). + MODEL(m) + return db.ExecuteOne[model.Communication](ctx, statement) +} +func CommunicationsFromOrganization(ctx context.Context, org_id int64) ([]*model.Communication, error) { + statement := table.Communication.SELECT( + table.Communication.AllColumns, + ).FROM(table.Communication). + WHERE(table.Communication.OrganizationID.EQ(postgres.Int(org_id))) + return db.ExecuteMany[model.Communication](ctx, statement) +} diff --git a/db/query/publicreport/report.go b/db/query/publicreport/report.go new file mode 100644 index 00000000..27db9f8f --- /dev/null +++ b/db/query/publicreport/report.go @@ -0,0 +1,32 @@ +package public + +import ( + "context" + //"time" + + //"github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/nidus-sync/db" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/table" + "github.com/go-jet/jet/v2/postgres" +) + +/* + func CommunicationInsert(ctx context.Context, txn bob.Tx, m *model.Communication) (*model.Communication, error) { + m.Created = time.Now() + statement := table.Communication.INSERT(table.Communication.MutableColumns). + MODEL(m) + return db.ExecuteOne[model.Communication](ctx, statement) + } +*/ +func PublicReportsFromIDs(ctx context.Context, report_ids []int64) ([]*model.Report, error) { + sql_ids := make([]postgres.Expression, len(report_ids)) + for i, report_id := range report_ids { + sql_ids[i] = postgres.Int(report_id) + } + statement := table.Report.SELECT( + table.Report.AllColumns, + ).FROM(table.Report). + WHERE(table.Report.ID.IN(sql_ids...)) + return db.ExecuteMany[model.Report](ctx, statement) +} diff --git a/html/embed.go b/html/embed.go index c207deb5..228e1bcc 100644 --- a/html/embed.go +++ b/html/embed.go @@ -76,9 +76,7 @@ func (ts templateSystemEmbed) loadTemplateSubdir(subdir string) error { } func (ts templateSystemEmbed) addSubdirTemplates(t *template.Template, subdir string) error { - var err error - //log.Debug().Msg("Adding subdir templates") - err = fs.WalkDir(ts.sourceFS, subdir, func(path string, d fs.DirEntry, err error) error { + var err error = fs.WalkDir(ts.sourceFS, subdir, func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { return err } diff --git a/label-studio/tasks_draft.go b/label-studio/tasks_draft.go index 2ca95816..0c838a0a 100644 --- a/label-studio/tasks_draft.go +++ b/label-studio/tasks_draft.go @@ -42,7 +42,7 @@ type Draft struct { func NewDraft(projectID int) *DraftRequest { return &DraftRequest{ DraftID: 0, - Project: string(projectID), + Project: fmt.Sprint(rune(projectID)), StartedAt: time.Now().UTC().Format(time.RFC3339Nano), } } diff --git a/llm/openai.go b/llm/openai.go index 77b3d9c7..22d72026 100644 --- a/llm/openai.go +++ b/llm/openai.go @@ -69,7 +69,7 @@ func (c *openAIClient) continueConversation(ctx context.Context, tools genai.Opt if m.String() == "" { //log.Debug().Msg("Tool called") } else { - var toSay string = m.String() + var toSay = m.String() toSay = strings.Replace(toSay, "report-mosquitoes-online: ", "", 1) return Message{ Content: toSay, diff --git a/middleware/logger.go b/middleware/logger.go index cff9bd20..dafdcc06 100644 --- a/middleware/logger.go +++ b/middleware/logger.go @@ -164,9 +164,7 @@ func (l *defaultLogEntry) Panic(v interface{}, stack []byte) { } func init() { - color := true - if runtime.GOOS == "windows" { - color = false - } + color := !(runtime.GOOS == "windows") + DefaultLogger = RequestLogger(&DefaultLogFormatter{Logger: log.New(os.Stdout, "", log.LstdFlags), NoColor: !color}) } diff --git a/middleware/wrap_writer.go b/middleware/wrap_writer.go index 367e0fcd..7b7d7186 100644 --- a/middleware/wrap_writer.go +++ b/middleware/wrap_writer.go @@ -146,7 +146,7 @@ type flushWriter struct { func (f *flushWriter) Flush() { f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) + fl := f.ResponseWriter.(http.Flusher) fl.Flush() } @@ -158,7 +158,7 @@ type hijackWriter struct { } func (f *hijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hj := f.basicWriter.ResponseWriter.(http.Hijacker) + hj := f.ResponseWriter.(http.Hijacker) return hj.Hijack() } @@ -171,12 +171,12 @@ type flushHijackWriter struct { func (f *flushHijackWriter) Flush() { f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) + fl := f.ResponseWriter.(http.Flusher) fl.Flush() } func (f *flushHijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hj := f.basicWriter.ResponseWriter.(http.Hijacker) + hj := f.ResponseWriter.(http.Hijacker) return hj.Hijack() } @@ -193,12 +193,12 @@ type httpFancyWriter struct { func (f *httpFancyWriter) Flush() { f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) + fl := f.ResponseWriter.(http.Flusher) fl.Flush() } func (f *httpFancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hj := f.basicWriter.ResponseWriter.(http.Hijacker) + hj := f.ResponseWriter.(http.Hijacker) return hj.Hijack() } @@ -207,15 +207,15 @@ func (f *http2FancyWriter) Push(target string, opts *http.PushOptions) error { } func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) { - if f.basicWriter.tee != nil { + if f.tee != nil { n, err := io.Copy(&f.basicWriter, r) - f.basicWriter.bytes += int(n) + f.bytes += int(n) return n, err } - rf := f.basicWriter.ResponseWriter.(io.ReaderFrom) - f.basicWriter.maybeWriteHeader() + rf := f.ResponseWriter.(io.ReaderFrom) + f.maybeWriteHeader() n, err := rf.ReadFrom(r) - f.basicWriter.bytes += int(n) + f.bytes += int(n) return n, err } @@ -234,7 +234,7 @@ type http2FancyWriter struct { func (f *http2FancyWriter) Flush() { f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) + fl := f.ResponseWriter.(http.Flusher) fl.Flush() } diff --git a/platform/arcgis.go b/platform/arcgis.go index 4c5bd0d9..9676aebc 100644 --- a/platform/arcgis.go +++ b/platform/arcgis.go @@ -792,9 +792,7 @@ func rowmapViaQuery(ctx context.Context, table string, sorted_columns []string, // +2 for geometry x and geometry x columnNames := make([]string, len(sorted_columns)+2) - for i, c := range sorted_columns { - columnNames[i] = c - } + copy(columnNames, sorted_columns) columnNames[len(sorted_columns)] = "geometry_x" columnNames[len(sorted_columns)+1] = "geometry_y" @@ -1031,7 +1029,7 @@ func selectAllFromQueryResult(table string, sorted_columns []string) string { return sb.String() } func toHistoryTable(table string) string { - return "History_" + table[3:len(table)] + return "History_" + table[3:] } func updateRowFromFeatureFS(ctx context.Context, transaction bob.Tx, table string, sorted_columns []string, feature *response.Feature) error { @@ -1618,7 +1616,7 @@ func aggregateAtResolution(ctx context.Context, resolution int, org_id int32, ty if err != nil { return fmt.Errorf("Failed to clear previous aggregation: %w", err) } - var to_insert []bob.Mod[*dialect.InsertQuery] = make([]bob.Mod[*dialect.InsertQuery], 0) + var to_insert = make([]bob.Mod[*dialect.InsertQuery], 0) to_insert = append(to_insert, im.Into("h3_aggregation", "cell", "resolution", "count_", "type_", "organization_id", "geometry")) for cell, count := range cellToCount { polygon, err := h3utils.CellToPostgisGeometry(cell) diff --git a/platform/communication.go b/platform/communication.go new file mode 100644 index 00000000..e8fa7902 --- /dev/null +++ b/platform/communication.go @@ -0,0 +1,12 @@ +package platform + +import ( + "context" + + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/public/model" + querypublic "github.com/Gleipnir-Technology/nidus-sync/db/query/public" +) + +func CommunicationsForOrganization(ctx context.Context, org_id int64) ([]*model.Communication, error) { + return querypublic.CommunicationsFromOrganization(ctx, org_id) +} diff --git a/platform/csv/pool.go b/platform/csv/pool.go index 74dccfb2..b5cc3368 100644 --- a/platform/csv/pool.go +++ b/platform/csv/pool.go @@ -156,7 +156,7 @@ func geocodePool(ctx context.Context, txn bob.Tx, client *stadia.StadiaMaps, job return nil } if geo.Address.Location == nil { - addError(ctx, txn, job.csv, pool.LineNumber, 0, fmt.Sprintf("nil location from geocoding")) + addError(ctx, txn, job.csv, pool.LineNumber, 0, "nil location from geocoding") return nil } geom_query := geom.PostgisPointQuery(*geo.Address.Location) @@ -329,7 +329,7 @@ func parseHeaders(row []string) ([]headerPoolEnum, []string) { ht := strings.TrimSpace(h) hl := strings.ToLower(ht) log.Debug().Str("header", hl).Msg("Saw CSV header") - var type_ headerPoolEnum = headerPoolTag + var type_ = headerPoolTag switch hl { case "city": type_ = headerPoolAddressLocality diff --git a/platform/label_studio.go b/platform/label_studio.go index a5af5dcf..826f8331 100644 --- a/platform/label_studio.go +++ b/platform/label_studio.go @@ -64,7 +64,7 @@ func createLabelStudioClient() (*labelstudio.Client, error) { // Get and store the access token err := labelStudioClient.GetAccessToken() if err != nil { - return nil, errors.New(fmt.Sprintf("Failed to get access token: %v", err)) + return nil, fmt.Errorf("Failed to get access token: %v", err) } log.Println("Got label studio client access token") @@ -116,7 +116,7 @@ func createTask(client *labelstudio.Client, project *labelstudio.Project, minioC return fmt.Errorf("Failed to upload audio: %v", err) } } - var transcription string = "" + var transcription = "" //if note.Transcription.IsValue() { //transcription = note.Transcription.MustGet() //} diff --git a/platform/publicreport.go b/platform/publicreport.go index 8f47f115..a44c3aca 100644 --- a/platform/publicreport.go +++ b/platform/publicreport.go @@ -15,7 +15,11 @@ import ( "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/gen/nidus-sync/public/model" + modelpublicreport "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" "github.com/Gleipnir-Technology/nidus-sync/db/models" + querypublic "github.com/Gleipnir-Technology/nidus-sync/db/query/public" + querypublicreport "github.com/Gleipnir-Technology/nidus-sync/db/query/publicreport" "github.com/aarondl/opt/omit" "github.com/aarondl/opt/omitnull" //"github.com/Gleipnir-Technology/nidus-sync/platform/background" @@ -214,6 +218,9 @@ func PublicReportReporterUpdated(ctx context.Context, org_id int32, report_id st func PublicReportsForOrganization(ctx context.Context, org_id int32, is_public bool) ([]*types.PublicReport, error) { return publicreport.ReportsForOrganization(ctx, org_id, is_public) } +func PublicReportsFromIDs(ctx context.Context, report_ids []int64) ([]*modelpublicreport.Report, error) { + return querypublicreport.PublicReportsFromIDs(ctx, report_ids) +} func PublicReportComplianceCreate(ctx context.Context, setter_report models.PublicreportReportSetter, setter_compliance models.PublicreportComplianceSetter, org_id int32) (*models.PublicreportReport, error) { return publicReportCreate(ctx, setter_report, nil, nil, nil, org_id, func(ctx context.Context, txn bob.Executor, report_id int32) error { setter_compliance.ReportID = omit.From(report_id) @@ -382,6 +389,15 @@ func publicReportCreate(ctx context.Context, setter_report models.PublicreportRe UserID: omitnull.FromPtr[int32](nil), }).One(ctx, txn) + comm := &model.Communication{ + SourceReportID: &result.ID, + } + comm, err = querypublic.CommunicationInsert(ctx, txn, comm) + if err != nil { + return nil, fmt.Errorf("insert communication: %w", err) + } + log.Debug().Int32("id", comm.ID).Msg("inserted new communication") + txn.Commit(ctx) event.Created( diff --git a/platform/publicreport/log.go b/platform/publicreport/log.go index 873cfd83..c163f9c0 100644 --- a/platform/publicreport/log.go +++ b/platform/publicreport/log.go @@ -59,9 +59,7 @@ func logEntriesByReportID(ctx context.Context, report_ids []int32, is_public boo if !ok { return results, fmt.Errorf("no text logs for %d", report_id) } - for _, l := range logs { - cur_logs = append(cur_logs, l) - } + cur_logs = append(cur_logs, logs...) results[report_id] = cur_logs } } diff --git a/platform/text/text.go b/platform/text/text.go index 97fb034b..bebb378b 100644 --- a/platform/text/text.go +++ b/platform/text/text.go @@ -38,10 +38,8 @@ func HandleTextMessage(ctx context.Context, source string, destination string, c if err != nil { return fmt.Errorf("Failed to get phone status") } - is_visible_to_llm := true - if status == enums.CommsPhonestatustypeUnconfirmed { - is_visible_to_llm = false - } + is_visible_to_llm := !(status == enums.CommsPhonestatustypeUnconfirmed) + l, err := models.CommsTextLogs.Insert(&models.CommsTextLogSetter{ //ID: Content: omit.From(content), diff --git a/platform/trap.go b/platform/trap.go index afc0ba6d..e51a4c5c 100644 --- a/platform/trap.go +++ b/platform/trap.go @@ -1,7 +1,6 @@ package platform import ( - "errors" "fmt" "time" @@ -257,7 +256,7 @@ func toTemplateTrapsNearby(locations []sql.TrapLocationBySourceIDRow, trap_data for _, td := range trap_data { c, ok := count_by_trap_data_id[td.Globalid] if !ok { - return results, errors.New(fmt.Sprintf("Failed to find trap count for %s", td.Globalid)) + return results, fmt.Errorf("Failed to find trap count for %s", td.Globalid) } loc_id := td.LocID count := &TrapCount{ @@ -278,7 +277,7 @@ func toTemplateTrapsNearby(locations []sql.TrapLocationBySourceIDRow, trap_data for _, location := range locations { counts, ok := counts_by_location_id[location.TrapLocationGlobalid] if !ok { - return results, errors.New(fmt.Sprintf("Failed to find counts for %s", location.TrapLocationGlobalid)) + return results, fmt.Errorf("Failed to find counts for %s", location.TrapLocationGlobalid) } trap := TrapNearby{ Counts: counts, diff --git a/resource/communication.go b/resource/communication.go index bd379f15..18b0bdb7 100644 --- a/resource/communication.go +++ b/resource/communication.go @@ -7,6 +7,7 @@ import ( "time" "github.com/Gleipnir-Technology/nidus-sync/config" + "github.com/Gleipnir-Technology/nidus-sync/db/gen/nidus-sync/publicreport/model" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" "github.com/google/uuid" @@ -25,14 +26,10 @@ func Communication(r *router) *communicationR { } type communication struct { - Created time.Time `json:"created"` - ID string `json:"id"` - PublicReport string `json:"public_report"` - Source string `json:"source"` - Type string `json:"type"` -} -type communicationList struct { - Communications []communication `json:"communications"` + Created time.Time `json:"created"` + ID int32 `json:"id"` + Source string `json:"source"` + Type string `json:"type"` } func toImageURLs(m map[string][]uuid.UUID, id string) []string { @@ -46,24 +43,61 @@ func toImageURLs(m map[string][]uuid.UUID, id string) []string { } return urls } -func (res *communicationR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) (*communicationList, *nhttp.ErrorWithStatus) { - reports, err := platform.PublicReportsForOrganization(ctx, user.Organization.ID, false) +func (res *communicationR) List(ctx context.Context, r *http.Request, user platform.User, query QueryParams) ([]*communication, *nhttp.ErrorWithStatus) { + comms, err := platform.CommunicationsForOrganization(ctx, int64(user.Organization.ID)) if err != nil { return nil, nhttp.NewError("nuisance report query: %w", err) } - comms := make([]communication, len(reports)) - for i, report := range reports { - populateDistrictURI(report, res.router) - populateReportURI(report, res.router, false) - comms[i] = communication{ - Created: report.Created, - ID: report.PublicID, - PublicReport: report.URI, - Type: "publicreport." + string(report.Type), + report_ids := make([]int64, 0) + for _, comm := range comms { + if comm.SourceReportID != nil { + report_ids = append(report_ids, int64(*comm.SourceReportID)) } } - _by_created := func(a, b communication) int { - if a.Created == b.Created { + public_reports, err := platform.PublicReportsFromIDs(ctx, report_ids) + if err != nil { + return nil, nhttp.NewError("public reports from IDs: %w", err) + } + public_report_id_to_report := make(map[int32]*model.Report, 0) + for _, pr := range public_reports { + public_report_id_to_report[pr.ID] = pr + } + result := make([]*communication, len(comms)) + for i, comm := range comms { + source_uri := "unknown" + type_ := "unknown" + if comm.SourceReportID != nil { + public_report, ok := public_report_id_to_report[*comm.SourceReportID] + if !ok { + return nil, nhttp.NewError("lookup report id %d failed", comm.SourceReportID) + } + source_uri, err = reportURI(res.router, "", public_report.PublicID) + if err != nil { + return nil, nhttp.NewError("gen report URI: %w", err) + } + type_ = "publicreport." + public_report.ReportType.String() + } else if comm.SourceEmailLogID != nil { + source_uri, err = emailURI(res.router, *comm.SourceEmailLogID) + if err != nil { + return nil, nhttp.NewError("gen email URI: %w", err) + } + type_ = "email" + } else if comm.SourceTextLogID != nil { + source_uri, err = textURI(res.router, *comm.SourceTextLogID) + if err != nil { + return nil, nhttp.NewError("gen email URI: %w", err) + } + source_uri = "text" + } + result[i] = &communication{ + Created: comm.Created, + ID: comm.ID, + Source: source_uri, + Type: type_, + } + } + _by_created := func(a, b *communication) int { + if a.Created.Equal(b.Created) { return 0 } else if a.Created.Before(b.Created) { return 1 @@ -71,8 +105,13 @@ func (res *communicationR) List(ctx context.Context, r *http.Request, user platf return -1 } } - slices.SortFunc(comms, _by_created) - return &communicationList{ - Communications: comms, - }, nil + slices.SortFunc(result, _by_created) + return result, nil +} + +func emailURI(r *router, id int32) (string, error) { + return "fake email uri", nil +} +func textURI(r *router, id int32) (string, error) { + return "fake text uri", nil } diff --git a/resource/publicreport.go b/resource/publicreport.go index 2d4732f1..879d8944 100644 --- a/resource/publicreport.go +++ b/resource/publicreport.go @@ -118,7 +118,7 @@ func reportURI(r *router, report_type string, public_id string) (string, error) case "water": route_name = "publicreport.water.ByIDGet" default: - return "", fmt.Errorf("Unrecognized report type '%s'", report_type) + route_name = "publicreport.ByIDGet" } uri, err := r.IDStrToURI(route_name, public_id) if err != nil { diff --git a/resource/query_params.go b/resource/query_params.go index c272822a..6c070ebf 100644 --- a/resource/query_params.go +++ b/resource/query_params.go @@ -20,10 +20,8 @@ func (qp QueryParams) SortOrDefault(default_name string, ascending bool) (string if s == "" { return default_name, ascending } - a := true - if s[0] == '-' { - a = false - } + a := !(s[0] == '-') + if s[0] == '+' || s[0] == '-' { s = s[1:] } diff --git a/resource/upload.go b/resource/upload.go index 53ea1c0c..53b4e28d 100644 --- a/resource/upload.go +++ b/resource/upload.go @@ -98,7 +98,7 @@ func (res *uploadR) Discard(ctx context.Context, r *http.Request, u platform.Use func (res *uploadR) PoolFlyoverCreate(ctx context.Context, r *http.Request, u platform.User, uploads []file.Upload) (string, *nhttp.ErrorWithStatus) { // If the organization we're uploading to doesn't have a service area, we can't process the upload correctly - if !(u.Organization.HasServiceArea() || u.Organization.IsCatchall()) { + if !u.Organization.HasServiceArea() && !u.Organization.IsCatchall() { return "", nhttp.NewErrorStatus(http.StatusConflict, "Your organization does not yet have a service area") } if len(uploads) == 0 { diff --git a/resource/user.go b/resource/user.go index 3e64a9d7..75952aa6 100644 --- a/resource/user.go +++ b/resource/user.go @@ -94,7 +94,7 @@ func (res *userR) ByIDPut(ctx context.Context, r *http.Request, user platform.Us return "", nhttp.NewErrorStatus(http.StatusBadRequest, "user id conversion: %w", err) } user_changes := &models.UserSetter{} - if !(user.HasRoot() || user.IsAccountOwner() || user.ID == user_id) { + if !user.HasRoot() && !user.IsAccountOwner() && user.ID != user_id { return "", nhttp.NewForbidden("Only account owners can change other users") } if updates.Avatar.IsValue() { diff --git a/tomtom/example/geocode-and-route/main.go b/tomtom/example/geocode-and-route/main.go index c8ae54f9..0780d493 100644 --- a/tomtom/example/geocode-and-route/main.go +++ b/tomtom/example/geocode-and-route/main.go @@ -51,9 +51,7 @@ func main() { log.Printf("%d: %d meters, %d seconds, %s traffic delay", i, s.LengthInMeters, s.TravelTimeInSeconds, s.TrafficDelayInSeconds) for _, leg := range route.Legs { all_stops = append(all_stops, leg.Points[0]) - for _, p := range leg.Points { - all_points = append(all_points, p) - } + all_points = append(all_points, leg.Points...) } } lines := tomtom.PolylineToGeoJSON(all_points) diff --git a/tomtom/geocode.go b/tomtom/geocode.go index ef46ea7f..17b7947e 100644 --- a/tomtom/geocode.go +++ b/tomtom/geocode.go @@ -10,10 +10,7 @@ type PointShort struct { } func (ps PointShort) AsPoint() Point { - return Point{ - Latitude: ps.Latitude, - Longitude: ps.Longitude, - } + return Point(ps) } type GeocodeResult struct { diff --git a/ts/components/CommunicationColumnList.vue b/ts/components/CommunicationColumnList.vue index 3a49c184..b96b34be 100644 --- a/ts/components/CommunicationColumnList.vue +++ b/ts/components/CommunicationColumnList.vue @@ -74,7 +74,7 @@ }" @click="handleClick(comm.id)" > - + @@ -91,7 +91,7 @@ diff --git a/ts/components/ListCardPublicReport.vue b/ts/components/ListCardPublicReport.vue deleted file mode 100644 index 7ee95e3d..00000000 --- a/ts/components/ListCardPublicReport.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - diff --git a/ts/format.ts b/ts/format.ts index f26df465..552fc00f 100644 --- a/ts/format.ts +++ b/ts/format.ts @@ -29,12 +29,14 @@ export function formatBigNumber(n: number): string { return result; } export function formatDate(date: Date): string { - return new Intl.DateTimeFormat("en-US", { + return new Intl.DateTimeFormat(undefined, { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", + hour12: false, + timeZoneName: "short", }).format(date); } diff --git a/ts/store/communication.ts b/ts/store/communication.ts index feb4b238..a0d35b4a 100644 --- a/ts/store/communication.ts +++ b/ts/store/communication.ts @@ -33,12 +33,10 @@ export const useCommunicationStore = defineStore("communication", () => { //if (typeFilter.value) params.append("type", typeFilter.value); const url = `${session.urls.api.communication}?${params}`; - const data = await apiClient.JSONGet(url); + const data = (await apiClient.JSONGet(url)) as CommunicationDTO[]; - all.value = data.communications.map((c: CommunicationDTO) => - Communication.fromJSON(c), - ); - return data.communications; + all.value = data.map((c: CommunicationDTO) => Communication.fromJSON(c)); + return all.value; } catch (err) { console.error("Error loading communications:", err); throw err; diff --git a/ts/type/api.ts b/ts/type/api.ts index 89f16ae3..daaaee7d 100644 --- a/ts/type/api.ts +++ b/ts/type/api.ts @@ -523,22 +523,22 @@ export class PublicReportWater extends PublicReport { export interface CommunicationDTO { created: string; id: string; - public_report?: string; + source: string; type: string; } export class Communication { constructor( public created: Date, public id: string, + public source: string, public type: string, - public public_report?: string, ) {} static fromJSON(json: CommunicationDTO): Communication { return new Communication( new Date(json.created), json.id, + json.source, json.type, - json.public_report, ); } } diff --git a/ts/view/Communication.vue b/ts/view/Communication.vue index 3e5a3570..02954466 100644 --- a/ts/view/Communication.vue +++ b/ts/view/Communication.vue @@ -97,7 +97,7 @@ const currentImage = computed(() => { }); const currentImages = computed(() => { const comm = selectedCommunication.value; - if (comm == null || comm.public_report == null) { + if (comm == null) { return []; } return selectedReport.value?.images ?? []; @@ -181,13 +181,12 @@ const selectedReport = computedAsync( async (): Promise => { if ( !( - selectedCommunication.value && selectedCommunication.value.public_report + selectedCommunication.value && + selectedCommunication.value.type != "publicreport" ) ) return; - return await storePublicReport.fetchByURI( - selectedCommunication.value.public_report, - ); + return await storePublicReport.byURI(selectedCommunication.value.source); }, ); const handleDeselect = (id: string) => {