From 7c414dab9d11cdc9cf37fc5cbb56ead257a6a4e7 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 20:02:16 +0200 Subject: [PATCH 01/32] feat: add action menu for detection management - Created new actionMenu template for per-detection actions - Added dropdown menu with options to review, ignore, or delete detections - Implemented Alpine.js and HTMX interactions for dynamic menu behavior - Updated listDetections and recentDetections views to include action menu column --- views/elements/actionMenu.html | 52 +++++++++++++++++++++++++++ views/fragments/listDetections.html | 8 +++++ views/fragments/recentDetections.html | 6 ++++ 3 files changed, 66 insertions(+) create mode 100644 views/elements/actionMenu.html diff --git a/views/elements/actionMenu.html b/views/elements/actionMenu.html new file mode 100644 index 00000000..548370d0 --- /dev/null +++ b/views/elements/actionMenu.html @@ -0,0 +1,52 @@ +{{define "actionMenu"}} + +{{end}} \ No newline at end of file diff --git a/views/fragments/listDetections.html b/views/fragments/listDetections.html index ceb1b1ed..0a7dc4f2 100644 --- a/views/fragments/listDetections.html +++ b/views/fragments/listDetections.html @@ -63,6 +63,9 @@ Recording + + + @@ -201,6 +204,11 @@ + + + + {{template "actionMenu" .}} + {{end}} diff --git a/views/fragments/recentDetections.html b/views/fragments/recentDetections.html index 47b2a501..00469c79 100644 --- a/views/fragments/recentDetections.html +++ b/views/fragments/recentDetections.html @@ -11,6 +11,7 @@ {{end}} Confidence Recording + @@ -112,6 +113,11 @@ + + + + {{template "actionMenu" .}} + {{end}} From cba40a89c92ce71a1b782bf252cbd8b9e8ddd111 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 20:26:53 +0200 Subject: [PATCH 02/32] refactor: remove unused DeleteNote handler from dashboard package --- internal/httpcontroller/handlers/dashboard.go | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/internal/httpcontroller/handlers/dashboard.go b/internal/httpcontroller/handlers/dashboard.go index 3b11916f..e9c3a2a0 100644 --- a/internal/httpcontroller/handlers/dashboard.go +++ b/internal/httpcontroller/handlers/dashboard.go @@ -2,10 +2,8 @@ package handlers import ( - "fmt" "log" "net/http" - "os" "sort" "strconv" "time" @@ -145,36 +143,3 @@ func (h *Handlers) GetAllNotes(c echo.Context) error { return c.JSON(http.StatusOK, notes) } - -// deleteNoteHandler deletes note object from database and its associated audio file -func (h *Handlers) DeleteNote(c echo.Context) error { - noteID := c.QueryParam("id") - if noteID == "" { - return h.NewHandlerError(fmt.Errorf("empty note ID"), "Note ID is required", http.StatusBadRequest) - } - - // Retrieve the path to the audio file before deleting the note - clipPath, err := h.DS.GetNoteClipPath(noteID) - if err != nil { - return h.NewHandlerError(err, "Failed to retrieve audio clip path", http.StatusInternalServerError) - } - - // Delete the note from the database - err = h.DS.Delete(noteID) - if err != nil { - return h.NewHandlerError(err, "Failed to delete note", http.StatusInternalServerError) - } - - // If there's an associated clip, delete the file - if clipPath != "" { - err = os.Remove(clipPath) - if err != nil { - h.logError(&HandlerError{Err: err, Message: "Failed to delete audio clip", Code: http.StatusInternalServerError}) - } else { - h.logInfo(fmt.Sprintf("Deleted audio clip: %s", clipPath)) - } - } - - // Pass this struct to the template or return a success message - return c.HTML(http.StatusOK, `
Delete successful!
`) -} From 47014bc7cc3e275568cb704f1980fd4a13a4d7f5 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 20:27:16 +0200 Subject: [PATCH 03/32] feat: add DeleteDetection handler for removing detections and associated files - Implemented new DeleteDetection method in detections handler - Supports asynchronous deletion of detection from database - Removes associated audio clip and spectrogram files - Provides SSE notifications for deletion status - Handles error cases and logging for file and database operations --- .../httpcontroller/handlers/detections.go | 117 ++++++++++-------- 1 file changed, 64 insertions(+), 53 deletions(-) diff --git a/internal/httpcontroller/handlers/detections.go b/internal/httpcontroller/handlers/detections.go index 83745fc1..73daa7bb 100644 --- a/internal/httpcontroller/handlers/detections.go +++ b/internal/httpcontroller/handlers/detections.go @@ -5,13 +5,14 @@ import ( "log" "math" "net/http" + "os" "runtime" + "strings" "time" "github.com/labstack/echo/v4" "github.com/tphakala/birdnet-go/internal/conf" "github.com/tphakala/birdnet-go/internal/datastore" - "github.com/tphakala/birdnet-go/internal/suncalc" "github.com/tphakala/birdnet-go/internal/weather" ) @@ -34,6 +35,68 @@ type NoteWithWeather struct { TimeOfDay weather.TimeOfDay } +// DeleteDetection handles the deletion of a detection and its associated files +func (h *Handlers) DeleteDetection(c echo.Context) error { + id := c.QueryParam("id") + if id == "" { + h.SSE.SendNotification(Notification{ + Message: "Missing detection ID", + Type: "error", + }) + return h.NewHandlerError(fmt.Errorf("no ID provided"), "Missing detection ID", http.StatusBadRequest) + } + + // Get the clip path before starting async deletion + clipPath, err := h.DS.GetNoteClipPath(id) + if err != nil { + h.SSE.SendNotification(Notification{ + Message: fmt.Sprintf("Failed to get clip path: %v", err), + Type: "error", + }) + return h.NewHandlerError(err, "Failed to get clip path", http.StatusInternalServerError) + } + + // Start async deletion + go func() { + // Delete the note from the database + if err := h.DS.Delete(id); err != nil { + h.SSE.SendNotification(Notification{ + Message: fmt.Sprintf("Failed to delete note: %v", err), + Type: "error", + }) + h.logger.Printf("Error deleting note %s: %v", id, err) + return + } + + // If there was a clip associated, delete the audio file and spectrogram + if clipPath != "" { + // Delete audio file + audioPath := fmt.Sprintf("%s/%s", h.Settings.Realtime.Audio.Export.Path, clipPath) + if err := os.Remove(audioPath); err != nil && !os.IsNotExist(err) { + h.logger.Printf("Warning: Failed to delete audio file %s: %v", audioPath, err) + } + + // Delete spectrogram file - stored in same directory with .png extension + spectrogramPath := fmt.Sprintf("%s/%s.png", h.Settings.Realtime.Audio.Export.Path, strings.TrimSuffix(clipPath, ".wav")) + if err := os.Remove(spectrogramPath); err != nil && !os.IsNotExist(err) { + h.logger.Printf("Warning: Failed to delete spectrogram file %s: %v", spectrogramPath, err) + } + } + + // Log the successful deletion + h.logger.Printf("Deleted detection %s", id) + + // Send success notification + h.SSE.SendNotification(Notification{ + Message: "Detection deleted successfully", + Type: "success", + }) + }() + + // Return immediate success response + return c.NoContent(http.StatusAccepted) +} + // ListDetections handles requests for hourly, species-specific, and search detections func (h *Handlers) Detections(c echo.Context) error { req := new(DetectionRequest) @@ -292,55 +355,3 @@ func (h *Handlers) addWeatherAndTimeOfDay(notes []datastore.Note) ([]NoteWithWea return notesWithWeather, nil } - -// getSunEvents calculates sun events for a given date -func (h *Handlers) getSunEvents(date string, loc *time.Location) (suncalc.SunEventTimes, error) { - // Parse the input date string into a time.Time object using the provided location - dateTime, err := time.ParseInLocation("2006-01-02", date, loc) - if err != nil { - // If parsing fails, return an empty SunEventTimes and the error - return suncalc.SunEventTimes{}, err - } - - // Attempt to get sun event times using the SunCalc - sunEvents, err := h.SunCalc.GetSunEventTimes(dateTime) - if err != nil { - // If sun events are not available, use default values - return suncalc.SunEventTimes{ - CivilDawn: dateTime.Add(5 * time.Hour), // Set civil dawn to 5:00 AM - Sunrise: dateTime.Add(6 * time.Hour), // Set sunrise to 6:00 AM - Sunset: dateTime.Add(18 * time.Hour), // Set sunset to 6:00 PM - CivilDusk: dateTime.Add(19 * time.Hour), // Set civil dusk to 7:00 PM - }, nil - } - - // Return the calculated sun events - return sunEvents, nil -} - -// findClosestWeather finds the closest hourly weather data to the given time -func findClosestWeather(noteTime time.Time, hourlyWeather []datastore.HourlyWeather) *datastore.HourlyWeather { - // If there's no weather data, return nil - if len(hourlyWeather) == 0 { - return nil - } - - // Initialize variables to track the closest weather data - var closestWeather *datastore.HourlyWeather - minDiff := time.Duration(math.MaxInt64) - - // Iterate through all hourly weather data - for i := range hourlyWeather { - // Calculate the absolute time difference between the note time and weather time - diff := noteTime.Sub(hourlyWeather[i].Time).Abs() - - // If this difference is smaller than the current minimum, update the closest weather - if diff < minDiff { - minDiff = diff - closestWeather = &hourlyWeather[i] - } - } - - // Return the weather data closest to the note time - return closestWeather -} From 0d344b378ad8acb46967d7216474cf637391d3f2 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 20:27:40 +0200 Subject: [PATCH 04/32] feat: add thread-safe settings mutex for concurrent access - Introduced settingsMutex to provide synchronized access to shared settings - Used sync.RWMutex to enable safe concurrent read and write operations - Prepares for potential multi-threaded settings management --- internal/httpcontroller/handlers/handlers.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/httpcontroller/handlers/handlers.go b/internal/httpcontroller/handlers/handlers.go index f0085f3c..95d0c806 100644 --- a/internal/httpcontroller/handlers/handlers.go +++ b/internal/httpcontroller/handlers/handlers.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "runtime/debug" + "sync" "github.com/labstack/echo/v4" "github.com/tphakala/birdnet-go/internal/conf" @@ -18,6 +19,8 @@ import ( "github.com/tphakala/birdnet-go/internal/suncalc" ) +var settingsMutex sync.RWMutex + // Handlers embeds baseHandler and includes all the dependencies needed for the application handlers. type Handlers struct { baseHandler From f072565b685079b4594385036f4fb417accd09fe Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 20:27:50 +0200 Subject: [PATCH 05/32] feat: add IgnoreSpecies handler for dynamic species exclusion - Implemented new IgnoreSpecies method in species handler - Supports adding and removing species from realtime exclusion list - Provides SSE notifications for species exclusion status - Handles saving and updating configuration settings - Ensures thread-safe settings modification --- internal/httpcontroller/handlers/species.go | 65 +++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 internal/httpcontroller/handlers/species.go diff --git a/internal/httpcontroller/handlers/species.go b/internal/httpcontroller/handlers/species.go new file mode 100644 index 00000000..4b6469ac --- /dev/null +++ b/internal/httpcontroller/handlers/species.go @@ -0,0 +1,65 @@ +package handlers + +import ( + "fmt" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/tphakala/birdnet-go/internal/conf" +) + +// IgnoreSpecies adds or removes a species from the excluded species list +func (h *Handlers) IgnoreSpecies(c echo.Context) error { + commonName := c.QueryParam("common_name") + if commonName == "" { + h.SSE.SendNotification(Notification{ + Message: "Missing species name", + Type: "error", + }) + return h.NewHandlerError(fmt.Errorf("missing species name"), "Missing species name", http.StatusBadRequest) + } + + // Get settings instance + settings := conf.Setting() + + // Check if species is already in the excluded list + isExcluded := false + for _, s := range settings.Realtime.Species.Exclude { + if s == commonName { + isExcluded = true + break + } + } + + if isExcluded { + // Remove from excluded list + newExcludeList := make([]string, 0) + for _, s := range settings.Realtime.Species.Exclude { + if s != commonName { + newExcludeList = append(newExcludeList, s) + } + } + settings.Realtime.Species.Exclude = newExcludeList + } else { + // Add to excluded list + settings.Realtime.Species.Exclude = append(settings.Realtime.Species.Exclude, commonName) + } + + // Save the settings + if err := conf.SaveSettings(); err != nil { + h.SSE.SendNotification(Notification{ + Message: fmt.Sprintf("Failed to save settings: %v", err), + Type: "error", + }) + return h.NewHandlerError(err, "Failed to save settings", http.StatusInternalServerError) + } + + // Send success notification + message := fmt.Sprintf("%s %s excluded species list", commonName, map[bool]string{true: "removed from", false: "added to"}[isExcluded]) + h.SSE.SendNotification(Notification{ + Message: message, + Type: "success", + }) + + return c.NoContent(http.StatusOK) +} From 17c43e9f488d0de4c6215a237239cff98ebeefee Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 20:28:13 +0200 Subject: [PATCH 06/32] feat: move weather-related utility functions to weather.go --- internal/httpcontroller/handlers/weather.go | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/internal/httpcontroller/handlers/weather.go b/internal/httpcontroller/handlers/weather.go index 86957df0..42e5f2a8 100644 --- a/internal/httpcontroller/handlers/weather.go +++ b/internal/httpcontroller/handlers/weather.go @@ -3,8 +3,10 @@ package handlers import ( "html/template" + "math" "time" + "github.com/tphakala/birdnet-go/internal/datastore" "github.com/tphakala/birdnet-go/internal/suncalc" "github.com/tphakala/birdnet-go/internal/weather" ) @@ -48,3 +50,55 @@ func (h *Handlers) GetWeatherDescriptionFunc() func(weatherCode string) string { return weather.GetIconDescription(iconCode) } } + +// getSunEvents calculates sun events for a given date +func (h *Handlers) getSunEvents(date string, loc *time.Location) (suncalc.SunEventTimes, error) { + // Parse the input date string into a time.Time object using the provided location + dateTime, err := time.ParseInLocation("2006-01-02", date, loc) + if err != nil { + // If parsing fails, return an empty SunEventTimes and the error + return suncalc.SunEventTimes{}, err + } + + // Attempt to get sun event times using the SunCalc + sunEvents, err := h.SunCalc.GetSunEventTimes(dateTime) + if err != nil { + // If sun events are not available, use default values + return suncalc.SunEventTimes{ + CivilDawn: dateTime.Add(5 * time.Hour), // Set civil dawn to 5:00 AM + Sunrise: dateTime.Add(6 * time.Hour), // Set sunrise to 6:00 AM + Sunset: dateTime.Add(18 * time.Hour), // Set sunset to 6:00 PM + CivilDusk: dateTime.Add(19 * time.Hour), // Set civil dusk to 7:00 PM + }, nil + } + + // Return the calculated sun events + return sunEvents, nil +} + +// findClosestWeather finds the closest hourly weather data to the given time +func findClosestWeather(noteTime time.Time, hourlyWeather []datastore.HourlyWeather) *datastore.HourlyWeather { + // If there's no weather data, return nil + if len(hourlyWeather) == 0 { + return nil + } + + // Initialize variables to track the closest weather data + var closestWeather *datastore.HourlyWeather + minDiff := time.Duration(math.MaxInt64) + + // Iterate through all hourly weather data + for i := range hourlyWeather { + // Calculate the absolute time difference between the note time and weather time + diff := noteTime.Sub(hourlyWeather[i].Time).Abs() + + // If this difference is smaller than the current minimum, update the closest weather + if diff < minDiff { + minDiff = diff + closestWeather = &hourlyWeather[i] + } + } + + // Return the weather data closest to the note time + return closestWeather +} From e216771b63722e2d62c8bd90df1bad62577a8e4a Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 20:31:14 +0200 Subject: [PATCH 07/32] feat: add routes and template function for species exclusion - Added DELETE route for detections deletion - Added POST route for ignoring species - Implemented template function to check species exclusion status - Supports dynamic species management in realtime settings --- internal/httpcontroller/routes.go | 7 ++++++- internal/httpcontroller/template_functions.go | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/httpcontroller/routes.go b/internal/httpcontroller/routes.go index a49a386f..694df555 100644 --- a/internal/httpcontroller/routes.go +++ b/internal/httpcontroller/routes.go @@ -116,10 +116,15 @@ func (s *Server) initRoutes() { // Special routes s.Echo.GET("/sse", s.Handlers.SSE.ServeSSE) s.Echo.GET("/audio-level", s.Handlers.WithErrorHandling(s.Handlers.AudioLevelSSE)) - s.Echo.DELETE("/note", h.WithErrorHandling(h.DeleteNote)) s.Echo.POST("/settings/save", h.WithErrorHandling(h.SaveSettings), s.AuthMiddleware) s.Echo.GET("/settings/audio/get", h.WithErrorHandling(h.GetAudioDevices), s.AuthMiddleware) + // Add DELETE method for detection deletion + s.Echo.DELETE("/detections/delete", h.WithErrorHandling(h.DeleteDetection)) + + // Add POST method for ignoring species + s.Echo.POST("/detections/ignore", h.WithErrorHandling(h.IgnoreSpecies)) + // Setup Error handler s.Echo.HTTPErrorHandler = func(err error, c echo.Context) { if handleErr := s.Handlers.HandleError(err, c); handleErr != nil { diff --git a/internal/httpcontroller/template_functions.go b/internal/httpcontroller/template_functions.go index 2e02b4e4..8759d452 100644 --- a/internal/httpcontroller/template_functions.go +++ b/internal/httpcontroller/template_functions.go @@ -54,6 +54,15 @@ func (s *Server) GetTemplateFunctions() template.FuncMap { "weatherDescription": s.Handlers.GetWeatherDescriptionFunc(), "getAllSpecies": s.GetAllSpecies, "getIncludedSpecies": s.GetIncludedSpecies, + "isSpeciesExcluded": func(commonName string) bool { + settings := conf.Setting() + for _, s := range settings.Realtime.Species.Exclude { + if s == commonName { + return true + } + } + return false + }, } } From fde2f4edf5a11ca8b7f6fcffee2dd406ee33ce8d Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 22:02:25 +0200 Subject: [PATCH 08/32] feat: add note verification status and update method - Added 'Verified' field to Note model with enum type - Implemented UpdateNote method in DataStore, MySQLStore, and SQLiteStore - Supports partial updates of note fields with flexible map-based approach --- internal/datastore/interfaces.go | 6 ++++++ internal/datastore/model.go | 1 + internal/datastore/mysql.go | 7 +++++++ internal/datastore/sqlite.go | 7 +++++++ 4 files changed, 21 insertions(+) diff --git a/internal/datastore/interfaces.go b/internal/datastore/interfaces.go index e6d049a0..835e44df 100644 --- a/internal/datastore/interfaces.go +++ b/internal/datastore/interfaces.go @@ -33,6 +33,7 @@ type Interface interface { GetNoteClipPath(noteID string) (string, error) DeleteNoteClipPath(noteID string) error GetClipsQualifyingForRemoval(minHours int, minClips int) ([]ClipForRemoval, error) + UpdateNote(id string, updates map[string]interface{}) error // weather data SaveDailyEvents(dailyEvents *DailyEvents) error GetDailyEvents(date string) (DailyEvents, error) @@ -550,3 +551,8 @@ func getHourRange(hour string, duration int) (startTime, endTime string) { endTime = fmt.Sprintf("%02d:00:00", endHour) return startTime, endTime } + +// UpdateNote updates specific fields of a note +func (ds *DataStore) UpdateNote(id string, updates map[string]interface{}) error { + return ds.DB.Model(&Note{}).Where("id = ?", id).Updates(updates).Error +} diff --git a/internal/datastore/model.go b/internal/datastore/model.go index af33bac8..53d02bd1 100644 --- a/internal/datastore/model.go +++ b/internal/datastore/model.go @@ -22,6 +22,7 @@ type Note struct { Threshold float64 Sensitivity float64 ClipName string + Verified string `gorm:"type:enum('unverified','correct','false_positive');default:'unverified'"` Comment string `gorm:"type:text"` ProcessingTime time.Duration Results []Results `gorm:"foreignKey:NoteID"` diff --git a/internal/datastore/mysql.go b/internal/datastore/mysql.go index 8bdcdf11..58d9b840 100644 --- a/internal/datastore/mysql.go +++ b/internal/datastore/mysql.go @@ -69,3 +69,10 @@ func (store *MySQLStore) Close() error { return nil } + +// UpdateNote updates specific fields of a note in MySQL +func (m *MySQLStore) UpdateNote(id string, updates map[string]interface{}) error { + return m.DB.Model(&Note{}).Where("id = ?", id).Updates(updates).Error +} + +// Save stores a note and its associated results as a single transaction in the database. diff --git a/internal/datastore/sqlite.go b/internal/datastore/sqlite.go index f819ef18..7b4cbb83 100644 --- a/internal/datastore/sqlite.go +++ b/internal/datastore/sqlite.go @@ -86,3 +86,10 @@ func (store *SQLiteStore) Close() error { return nil } + +// UpdateNote updates specific fields of a note in SQLite +func (s *SQLiteStore) UpdateNote(id string, updates map[string]interface{}) error { + return s.DB.Model(&Note{}).Where("id = ?", id).Updates(updates).Error +} + +// Save stores a note and its associated results as a single transaction in the database. From 714a80dcb5d30290a0d2481cb5c7733185b2fef8 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 22:02:55 +0200 Subject: [PATCH 09/32] feat: add route for reviewing detections - Added POST route `/detections/review` to support detection review functionality - Integrated ReviewDetection handler with error handling middleware --- internal/httpcontroller/routes.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/httpcontroller/routes.go b/internal/httpcontroller/routes.go index 694df555..735852b3 100644 --- a/internal/httpcontroller/routes.go +++ b/internal/httpcontroller/routes.go @@ -125,6 +125,9 @@ func (s *Server) initRoutes() { // Add POST method for ignoring species s.Echo.POST("/detections/ignore", h.WithErrorHandling(h.IgnoreSpecies)) + // Add POST method for reviewing detections + s.Echo.POST("/detections/review", h.WithErrorHandling(h.ReviewDetection)) + // Setup Error handler s.Echo.HTTPErrorHandler = func(err error, c echo.Context) { if handleErr := s.Handlers.HandleError(err, c); handleErr != nil { From fe06abe0b17993ae2a89b4c5db7ee92a1fe74741 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 22:05:03 +0200 Subject: [PATCH 10/32] feat: improve cache and vary headers for HTMX requests - Skip cache control for HTMX requests - Always set Vary header for HTMX requests - Ensure HTMX headers are preserved with no-store cache control --- internal/httpcontroller/middleware.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/internal/httpcontroller/middleware.go b/internal/httpcontroller/middleware.go index b560b2e4..b57e4b18 100644 --- a/internal/httpcontroller/middleware.go +++ b/internal/httpcontroller/middleware.go @@ -28,6 +28,11 @@ func (s *Server) configureMiddleware() { func (s *Server) CacheControlMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { + // Skip cache control for HTMX requests + if c.Request().Header.Get("HX-Request") != "" { + return next(c) + } + path := c.Request().URL.Path s.Debug("CacheControlMiddleware: Processing request for path: %s", path) @@ -73,10 +78,16 @@ func (s *Server) CacheControlMiddleware() echo.MiddlewareFunc { func VaryHeaderMiddleware() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { + // Always set Vary header for HTMX requests + c.Response().Header().Set("Vary", "HX-Request") + + // Ensure HTMX headers are preserved if c.Request().Header.Get("HX-Request") != "" { - c.Response().Header().Set("Vary", "HX-Request") + c.Response().Header().Set("Cache-Control", "no-store") } - return next(c) + + err := next(c) + return err } } } From 97b7484cbd82cc2e39387d192e102973e3af8b06 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 22:07:35 +0200 Subject: [PATCH 11/32] feat: enhance detection handlers with improved logging and error handling - Refactored DeleteDetection handler to synchronous execution - Added ReviewDetection handler for marking detections as correct or false positive - Improved logging using h.Debug() method - Updated response handling with HTMX headers - Simplified error handling and notification mechanisms --- .../httpcontroller/handlers/detections.go | 132 ++++++++++++------ 1 file changed, 88 insertions(+), 44 deletions(-) diff --git a/internal/httpcontroller/handlers/detections.go b/internal/httpcontroller/handlers/detections.go index 73daa7bb..39435c80 100644 --- a/internal/httpcontroller/handlers/detections.go +++ b/internal/httpcontroller/handlers/detections.go @@ -38,6 +38,7 @@ type NoteWithWeather struct { // DeleteDetection handles the deletion of a detection and its associated files func (h *Handlers) DeleteDetection(c echo.Context) error { id := c.QueryParam("id") + if id == "" { h.SSE.SendNotification(Notification{ Message: "Missing detection ID", @@ -46,9 +47,10 @@ func (h *Handlers) DeleteDetection(c echo.Context) error { return h.NewHandlerError(fmt.Errorf("no ID provided"), "Missing detection ID", http.StatusBadRequest) } - // Get the clip path before starting async deletion + // Get the clip path before deletion clipPath, err := h.DS.GetNoteClipPath(id) if err != nil { + h.Debug("Failed to get clip path: %v", err) h.SSE.SendNotification(Notification{ Message: fmt.Sprintf("Failed to get clip path: %v", err), Type: "error", @@ -56,45 +58,43 @@ func (h *Handlers) DeleteDetection(c echo.Context) error { return h.NewHandlerError(err, "Failed to get clip path", http.StatusInternalServerError) } - // Start async deletion - go func() { - // Delete the note from the database - if err := h.DS.Delete(id); err != nil { - h.SSE.SendNotification(Notification{ - Message: fmt.Sprintf("Failed to delete note: %v", err), - Type: "error", - }) - h.logger.Printf("Error deleting note %s: %v", id, err) - return - } + // Delete the note from the database + if err := h.DS.Delete(id); err != nil { + h.Debug("Failed to delete note %s: %v", id, err) + h.SSE.SendNotification(Notification{ + Message: fmt.Sprintf("Failed to delete note: %v", err), + Type: "error", + }) + return h.NewHandlerError(err, "Failed to delete note", http.StatusInternalServerError) + } - // If there was a clip associated, delete the audio file and spectrogram - if clipPath != "" { - // Delete audio file - audioPath := fmt.Sprintf("%s/%s", h.Settings.Realtime.Audio.Export.Path, clipPath) - if err := os.Remove(audioPath); err != nil && !os.IsNotExist(err) { - h.logger.Printf("Warning: Failed to delete audio file %s: %v", audioPath, err) - } + // If there was a clip associated, delete the audio file and spectrogram + if clipPath != "" { + // Delete audio file + audioPath := fmt.Sprintf("%s/%s", h.Settings.Realtime.Audio.Export.Path, clipPath) + if err := os.Remove(audioPath); err != nil && !os.IsNotExist(err) { + h.Debug("Failed to delete audio file %s: %v", audioPath, err) + } - // Delete spectrogram file - stored in same directory with .png extension - spectrogramPath := fmt.Sprintf("%s/%s.png", h.Settings.Realtime.Audio.Export.Path, strings.TrimSuffix(clipPath, ".wav")) - if err := os.Remove(spectrogramPath); err != nil && !os.IsNotExist(err) { - h.logger.Printf("Warning: Failed to delete spectrogram file %s: %v", spectrogramPath, err) - } + // Delete spectrogram file + spectrogramPath := fmt.Sprintf("%s/%s.png", h.Settings.Realtime.Audio.Export.Path, strings.TrimSuffix(clipPath, ".wav")) + if err := os.Remove(spectrogramPath); err != nil && !os.IsNotExist(err) { + h.Debug("Failed to delete spectrogram file %s: %v", spectrogramPath, err) } + } - // Log the successful deletion - h.logger.Printf("Deleted detection %s", id) + // Log the successful deletion + h.Debug("Successfully deleted detection %s", id) - // Send success notification - h.SSE.SendNotification(Notification{ - Message: "Detection deleted successfully", - Type: "success", - }) - }() + // Send success notification + h.SSE.SendNotification(Notification{ + Message: "Detection deleted successfully", + Type: "success", + }) - // Return immediate success response - return c.NoContent(http.StatusAccepted) + // Set response headers + c.Response().Header().Set("HX-Trigger", "refreshList") + return c.NoContent(http.StatusOK) } // ListDetections handles requests for hourly, species-specific, and search detections @@ -258,34 +258,36 @@ func (h *Handlers) DetectionDetails(c echo.Context) error { } // RecentDetections handles requests for the latest detections. -// It retrieves the last set of detections based on the specified count and view type. func (h *Handlers) RecentDetections(c echo.Context) error { - numDetections := parseNumDetections(c.QueryParam("numDetections"), 10) // Default value is 10 + h.Debug("RecentDetections: Starting handler") - var data interface{} - var templateName string + numDetections := parseNumDetections(c.QueryParam("numDetections"), 10) + h.Debug("RecentDetections: Fetching %d detections", numDetections) - // Use the existing detailed view notes, err := h.DS.GetLastDetections(numDetections) if err != nil { + h.Debug("RecentDetections: Error fetching detections: %v", err) return h.NewHandlerError(err, "Failed to fetch recent detections", http.StatusInternalServerError) } - data = struct { + h.Debug("RecentDetections: Found %d detections", len(notes)) + + data := struct { Notes []datastore.Note DashboardSettings conf.Dashboard }{ Notes: notes, DashboardSettings: *h.DashboardSettings, } - templateName = "recentDetections" - // Render the appropriate template with the data - err = c.Render(http.StatusOK, templateName, data) + h.Debug("RecentDetections: Rendering template") + err = c.Render(http.StatusOK, "recentDetections", data) if err != nil { - log.Printf("Failed to render %s template: %v", templateName, err) + h.Debug("RecentDetections: Error rendering template: %v", err) return h.NewHandlerError(err, "Failed to render template", http.StatusInternalServerError) } + + h.Debug("RecentDetections: Successfully completed") return nil } @@ -355,3 +357,45 @@ func (h *Handlers) addWeatherAndTimeOfDay(notes []datastore.Note) ([]NoteWithWea return notesWithWeather, nil } + +// ReviewDetection handles the verification of a detection as either correct or false positive +func (h *Handlers) ReviewDetection(c echo.Context) error { + id := c.FormValue("id") + if id == "" { + return h.NewHandlerError(fmt.Errorf("no ID provided"), "Missing detection ID", http.StatusBadRequest) + } + + verified := c.FormValue("verified") + if verified != "correct" && verified != "false_positive" { + return h.NewHandlerError(fmt.Errorf("invalid verification status"), "Invalid verification status", http.StatusBadRequest) + } + + comment := c.FormValue("comment") + + // Verify that the note exists + if _, err := h.DS.Get(id); err != nil { + return h.NewHandlerError(err, "Failed to retrieve note", http.StatusInternalServerError) + } + + // Update only the verification status and comment + if err := h.DS.UpdateNote(id, map[string]interface{}{ + "verified": verified, + "comment": comment, + }); err != nil { + h.SSE.SendNotification(Notification{ + Message: fmt.Sprintf("Failed to save review: %v", err), + Type: "error", + }) + return h.NewHandlerError(err, "Failed to save note", http.StatusInternalServerError) + } + + // Send success notification + h.SSE.SendNotification(Notification{ + Message: "Detection review saved successfully", + Type: "success", + }) + + // Set HTMX response headers + c.Response().Header().Set("HX-Refresh", "true") + return c.NoContent(http.StatusOK) +} From 1aef7485a5ce431aa3a4ed40000cd1eca52e6621 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 22:08:16 +0200 Subject: [PATCH 12/32] feat: improve recent detections loading with HTMX - Updated dashboard.html to use HTMX for dynamic recent detections loading - Added hx-trigger="load refreshList" to automatically fetch initial detections - Modified target selector to use a wrapper div for more flexible content updates - Simplified HTMX attribute configuration for better readability --- views/dashboard.html | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/views/dashboard.html b/views/dashboard.html index 37d39018..dfa92f9c 100644 --- a/views/dashboard.html +++ b/views/dashboard.html @@ -47,9 +47,11 @@
Recent Detections - - @@ -58,9 +60,11 @@
- -
- + +
From 245df027df10e4a28c4f573de0829b4d396754ef Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 22:09:31 +0200 Subject: [PATCH 13/32] feat: add HTMX-powered dynamic refresh for recent detections list - Added loading indicator for list refresh - Implemented HTMX trigger for automatic list updates - Enhanced responsive design with dynamic content loading - Improved user experience with seamless list refreshing --- views/fragments/recentDetections.html | 380 ++++++++++++++------------ 1 file changed, 198 insertions(+), 182 deletions(-) diff --git a/views/fragments/recentDetections.html b/views/fragments/recentDetections.html index 00469c79..80ff2063 100644 --- a/views/fragments/recentDetections.html +++ b/views/fragments/recentDetections.html @@ -1,210 +1,226 @@ {{define "recentDetections"}} - - - - - - - {{if .DashboardSettings.Thumbnails.Recent}} - - {{end}} - - - - - - - {{range .Notes}} - - - - - - - - - +
+ + +
+
+
+ Updating list... +
+
- - {{if $.DashboardSettings.Thumbnails.Recent}} -
+ + {{end}} + + + + +
+ {{range .Notes}} +
+ +
+ + {{.Time}} - - {{if $.DashboardSettings.Thumbnails.Recent}} - - {{end}} -
- - - -
- - Spectrogram - - -
- - -
- -
- -
-
+ +
+
+ +
+
+ {{confidence .Confidence}}
- 0:00 - - - - +
+ + + {{if $.DashboardSettings.Thumbnails.Recent}} + + {{end}}
+ + + +
+ + Spectrogram + + +
+ + +
+ +
+ +
+
+
+ 0:00 + + + + + +
+
+ +
+ {{end}}
- {{end}} -
{{end}} \ No newline at end of file From f43eb3895c2216b10f6766e64dd719bb9216735a Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 22:47:39 +0200 Subject: [PATCH 14/32] feat: enhance detection action menu with modals and dynamic interactions - Replaced HTMX-driven actions with Alpine.js and modal-based interactions - Added review and confirmation modals for detection actions - Implemented dynamic event handling for species exclusion and detection deletion - Removed console logging and improved user interaction flow - Added SSE event listeners for modal management --- views/elements/actionMenu.html | 99 ++++++++++++++++++++++++++------ views/elements/confirmModal.html | 17 ++++++ views/elements/reviewModal.html | 91 +++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 views/elements/confirmModal.html create mode 100644 views/elements/reviewModal.html diff --git a/views/elements/actionMenu.html b/views/elements/actionMenu.html index 548370d0..299f29cc 100644 --- a/views/elements/actionMenu.html +++ b/views/elements/actionMenu.html @@ -4,49 +4,114 @@ open: false, isExcluded: {{if isSpeciesExcluded .CommonName}}true{{else}}false{{end}}, init() { - console.log('Menu initialized for {{.CommonName}}, isExcluded:', this.isExcluded); + //console.log('Menu initialized for {{.CommonName}}, isExcluded:', this.isExcluded); document.body.addEventListener('species-excluded-{{.CommonName}}', () => { - console.log('Received exclude event for {{.CommonName}}'); + //console.log('Received exclude event for {{.CommonName}}'); this.isExcluded = true; }); document.body.addEventListener('species-included-{{.CommonName}}', () => { - console.log('Received include event for {{.CommonName}}'); + //console.log('Received include event for {{.CommonName}}'); this.isExcluded = false; }); } }" x-init="init()"> - + + + {{template "reviewModal" .}} + + + {{template "confirmModal" .}} + + +
{{end}} \ No newline at end of file diff --git a/views/elements/confirmModal.html b/views/elements/confirmModal.html new file mode 100644 index 00000000..2b2b5a4a --- /dev/null +++ b/views/elements/confirmModal.html @@ -0,0 +1,17 @@ +{{define "confirmModal"}} + + + + +{{end}} \ No newline at end of file diff --git a/views/elements/reviewModal.html b/views/elements/reviewModal.html new file mode 100644 index 00000000..aea3de28 --- /dev/null +++ b/views/elements/reviewModal.html @@ -0,0 +1,91 @@ +{{define "reviewModal"}} + + + + +{{end}} \ No newline at end of file From 5d14947c3306b8fe5e5cdfdc0e2055ac1fc6bdde Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sat, 25 Jan 2025 22:48:00 +0200 Subject: [PATCH 15/32] feat: add global loading indicator for HTMX interactions - Introduced a centralized loading indicator for consistent UX across views - Added global loading indicator to dashboard and recent detections sections - Implemented responsive and styled loading spinner with text - Removed redundant local loading indicators - Enhanced HTMX interactions with consistent loading feedback --- views/dashboard.html | 2 ++ views/fragments/listDetections.html | 6 +++++- views/fragments/recentDetections.html | 10 +--------- views/index.html | 10 ++++++++++ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/views/dashboard.html b/views/dashboard.html index dfa92f9c..e2f30829 100644 --- a/views/dashboard.html +++ b/views/dashboard.html @@ -50,6 +50,7 @@
From a9ab41f98dcfd25baa74c6bfbd3bf0ec0ef10724 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sun, 26 Jan 2025 16:37:07 +0200 Subject: [PATCH 21/32] refactor: remove redundant wrapper container from recent detections view --- views/fragments/recentDetections.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/views/fragments/recentDetections.html b/views/fragments/recentDetections.html index 0d6dceeb..5009e8b6 100644 --- a/views/fragments/recentDetections.html +++ b/views/fragments/recentDetections.html @@ -1,13 +1,5 @@ {{define "recentDetections"}} -
- From 7dd087045e4fd434c53d433d42a3de03d269e6b4 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sun, 26 Jan 2025 16:37:59 +0200 Subject: [PATCH 22/32] refactor: improve index.html HTMX and JavaScript formatting - Cleaned up whitespace and formatting in HTMX attributes - Added consistent spacing for conditional HTMX triggers - Improved readability of JavaScript security redirect logic - Minor structural improvements to HTML template rendering --- views/index.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/views/index.html b/views/index.html index da7b60d9..cb49387a 100644 --- a/views/index.html +++ b/views/index.html @@ -52,10 +52,12 @@ hx-get="{{.PreloadFragment}}" hx-target="#loginModal" hx-on::after-request="loginModal.showModal()" - {{else if .PreloadFragment}} hx-trigger="load" + {{else if .PreloadFragment}} + hx-trigger="load" hx-get="{{.PreloadFragment}}" hx-target="this" {{end}}> + {{ RenderContent . }} @@ -101,7 +103,7 @@ this.checked = localStorage.getItem('theme') === 'dark'; }); - {{ if .Settings.Security.RedirectToHTTPS}} + {{ if .Settings.Security.RedirectToHTTPS }} // Check for HTTPS redirect (function () { if (window.location.protocol !== 'https:' && @@ -111,6 +113,7 @@ } })(); {{ end }} + From 54d519e47bb4e0990bf89683444026778260e99d Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sun, 26 Jan 2025 16:38:11 +0200 Subject: [PATCH 23/32] chore: update HTMX library to version 2.0 - Bumped HTMX library version in Taskfile.yml - Updated asset download to fetch latest 2.0 release --- Taskfile.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Taskfile.yml b/Taskfile.yml index 0ac98370..50cd5434 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -78,7 +78,7 @@ tasks: - mkdir -p assets - curl -sL https://unpkg.com/leaflet/dist/leaflet.js -o assets/leaflet.js - curl -sL https://unpkg.com/leaflet/dist/leaflet.css -o assets/leaflet.css - - curl -sL https://unpkg.com/htmx.org -o assets/htmx.min.js + - curl -sL https://unpkg.com/htmx.org@2.0 -o assets/htmx.min.js - curl -sL https://unpkg.com/alpinejs -o assets/alpinejs.min.js generate-tailwindcss: From 9c766982b761d3eb03fb4eb24fed5f7c99d5cfe5 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sun, 26 Jan 2025 16:39:16 +0200 Subject: [PATCH 24/32] refactor: update detection action response headers - Changed HX-Trigger header value from "refreshList" to "refreshListEvent" - Simplified response header setting for delete and review detection methods - Ensured consistent event naming for HTMX list refresh mechanism --- .../httpcontroller/handlers/detections.go | 130 +++++++++--------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/internal/httpcontroller/handlers/detections.go b/internal/httpcontroller/handlers/detections.go index 39435c80..0936878a 100644 --- a/internal/httpcontroller/handlers/detections.go +++ b/internal/httpcontroller/handlers/detections.go @@ -35,68 +35,6 @@ type NoteWithWeather struct { TimeOfDay weather.TimeOfDay } -// DeleteDetection handles the deletion of a detection and its associated files -func (h *Handlers) DeleteDetection(c echo.Context) error { - id := c.QueryParam("id") - - if id == "" { - h.SSE.SendNotification(Notification{ - Message: "Missing detection ID", - Type: "error", - }) - return h.NewHandlerError(fmt.Errorf("no ID provided"), "Missing detection ID", http.StatusBadRequest) - } - - // Get the clip path before deletion - clipPath, err := h.DS.GetNoteClipPath(id) - if err != nil { - h.Debug("Failed to get clip path: %v", err) - h.SSE.SendNotification(Notification{ - Message: fmt.Sprintf("Failed to get clip path: %v", err), - Type: "error", - }) - return h.NewHandlerError(err, "Failed to get clip path", http.StatusInternalServerError) - } - - // Delete the note from the database - if err := h.DS.Delete(id); err != nil { - h.Debug("Failed to delete note %s: %v", id, err) - h.SSE.SendNotification(Notification{ - Message: fmt.Sprintf("Failed to delete note: %v", err), - Type: "error", - }) - return h.NewHandlerError(err, "Failed to delete note", http.StatusInternalServerError) - } - - // If there was a clip associated, delete the audio file and spectrogram - if clipPath != "" { - // Delete audio file - audioPath := fmt.Sprintf("%s/%s", h.Settings.Realtime.Audio.Export.Path, clipPath) - if err := os.Remove(audioPath); err != nil && !os.IsNotExist(err) { - h.Debug("Failed to delete audio file %s: %v", audioPath, err) - } - - // Delete spectrogram file - spectrogramPath := fmt.Sprintf("%s/%s.png", h.Settings.Realtime.Audio.Export.Path, strings.TrimSuffix(clipPath, ".wav")) - if err := os.Remove(spectrogramPath); err != nil && !os.IsNotExist(err) { - h.Debug("Failed to delete spectrogram file %s: %v", spectrogramPath, err) - } - } - - // Log the successful deletion - h.Debug("Successfully deleted detection %s", id) - - // Send success notification - h.SSE.SendNotification(Notification{ - Message: "Detection deleted successfully", - Type: "success", - }) - - // Set response headers - c.Response().Header().Set("HX-Trigger", "refreshList") - return c.NoContent(http.StatusOK) -} - // ListDetections handles requests for hourly, species-specific, and search detections func (h *Handlers) Detections(c echo.Context) error { req := new(DetectionRequest) @@ -358,6 +296,69 @@ func (h *Handlers) addWeatherAndTimeOfDay(notes []datastore.Note) ([]NoteWithWea return notesWithWeather, nil } +// DeleteDetection handles the deletion of a detection and its associated files +func (h *Handlers) DeleteDetection(c echo.Context) error { + id := c.QueryParam("id") + + if id == "" { + h.SSE.SendNotification(Notification{ + Message: "Missing detection ID", + Type: "error", + }) + return h.NewHandlerError(fmt.Errorf("no ID provided"), "Missing detection ID", http.StatusBadRequest) + } + + // Get the clip path before deletion + clipPath, err := h.DS.GetNoteClipPath(id) + if err != nil { + h.Debug("Failed to get clip path: %v", err) + h.SSE.SendNotification(Notification{ + Message: fmt.Sprintf("Failed to get clip path: %v", err), + Type: "error", + }) + return h.NewHandlerError(err, "Failed to get clip path", http.StatusInternalServerError) + } + + // Delete the note from the database + if err := h.DS.Delete(id); err != nil { + h.Debug("Failed to delete note %s: %v", id, err) + h.SSE.SendNotification(Notification{ + Message: fmt.Sprintf("Failed to delete note: %v", err), + Type: "error", + }) + return h.NewHandlerError(err, "Failed to delete note", http.StatusInternalServerError) + } + + // If there was a clip associated, delete the audio file and spectrogram + if clipPath != "" { + // Delete audio file + audioPath := fmt.Sprintf("%s/%s", h.Settings.Realtime.Audio.Export.Path, clipPath) + if err := os.Remove(audioPath); err != nil && !os.IsNotExist(err) { + h.Debug("Failed to delete audio file %s: %v", audioPath, err) + } + + // Delete spectrogram file + spectrogramPath := fmt.Sprintf("%s/%s.png", h.Settings.Realtime.Audio.Export.Path, strings.TrimSuffix(clipPath, ".wav")) + if err := os.Remove(spectrogramPath); err != nil && !os.IsNotExist(err) { + h.Debug("Failed to delete spectrogram file %s: %v", spectrogramPath, err) + } + } + + // Log the successful deletion + h.Debug("Successfully deleted detection %s", id) + + // Send success notification + h.SSE.SendNotification(Notification{ + Message: "Detection deleted successfully", + Type: "success", + }) + + // Set response header to refresh list + c.Response().Header().Set("HX-Trigger", "refreshListEvent") + + return c.NoContent(http.StatusOK) +} + // ReviewDetection handles the verification of a detection as either correct or false positive func (h *Handlers) ReviewDetection(c echo.Context) error { id := c.FormValue("id") @@ -395,7 +396,8 @@ func (h *Handlers) ReviewDetection(c echo.Context) error { Type: "success", }) - // Set HTMX response headers - c.Response().Header().Set("HX-Refresh", "true") + // Set response header to refresh list + c.Response().Header().Set("HX-Trigger", "refreshListEvent") + return c.NoContent(http.StatusOK) } From 15f1c482e3b199d67c2ec786aea7ad234a8d68bd Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sun, 26 Jan 2025 17:09:19 +0200 Subject: [PATCH 25/32] feat: enhance review modal with species ignore functionality - Added new section for ignoring specific species in false positive detections - Implemented dynamic show/hide of ignore species option based on detection verification - Added checkbox to exclude species from future detections - Included informative text explaining species ignore behavior - Refined radio button styling with smaller radio inputs --- views/elements/reviewModal.html | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/views/elements/reviewModal.html b/views/elements/reviewModal.html index 57785dc5..c5de4d87 100644 --- a/views/elements/reviewModal.html +++ b/views/elements/reviewModal.html @@ -57,17 +57,28 @@

Review Detection: {{.CommonName | js}}

> -
+
+ +
+ +
+ Ignoring this species will prevent future detections of species. This will not remove existing detections. +
+
+
@@ -110,10 +113,19 @@ {{if ne $.QueryType "species"}} {{end}} @@ -135,11 +147,20 @@ @@ -227,7 +248,7 @@ {{if gt .CurrentPage 1}} {{else}} @@ -238,7 +259,7 @@ {{if lt .CurrentPage .TotalPages}} {{else}} @@ -255,5 +276,7 @@
- - {{.CommonName}} - +
+ + {{.CommonName}} + + {{if .Verified}} + {{if eq .Verified "correct"}} + + {{else if eq .Verified "false_positive"}} + + {{end}} + {{end}} +
-
- - {{confidence .Confidence}} - +
+ + {{if .Verified}} + {{if eq .Verified "correct"}} +
+ {{else if eq .Verified "false_positive"}} +
+ {{end}} + {{end}}
+
+ {{end}} \ No newline at end of file From cd1b2d0332e55ef0bf0e1c2f1c3b19f6d599684c Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sun, 26 Jan 2025 18:14:50 +0200 Subject: [PATCH 29/32] refactor: optimize recent detections view layout and verification display - Restructured confidence and verification indicators with new container classes - Simplified mobile view confidence display - Removed redundant thumbnail container in mobile view - Enhanced visual hierarchy for detection verification status --- views/fragments/recentDetections.html | 43 +++++++++++++++------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/views/fragments/recentDetections.html b/views/fragments/recentDetections.html index 5009e8b6..4482ca3a 100644 --- a/views/fragments/recentDetections.html +++ b/views/fragments/recentDetections.html @@ -48,8 +48,17 @@ -
- {{ confidence .Confidence}} +
+
+ {{ confidence .Confidence}} +
+ {{if .Verified}} + {{if eq .Verified "correct"}} +
+ {{else if eq .Verified "false_positive"}} +
+ {{end}} + {{end}}
@@ -138,25 +147,21 @@ {{title .CommonName}} - -
-
- -
-
- {{confidence .Confidence}} + +
+
+
+ {{confidence .Confidence}} +
+ {{if .Verified}} + {{if eq .Verified "correct"}} +
+ {{else if eq .Verified "false_positive"}} +
+ {{end}} + {{end}}
- - - {{if $.DashboardSettings.Thumbnails.Recent}} -
- - - -
- {{end}}
From 7cd645a52410ee82247eeabc11e581982b0e5f11 Mon Sep 17 00:00:00 2001 From: "Tomi P. Hakala" Date: Sun, 26 Jan 2025 18:14:59 +0200 Subject: [PATCH 30/32] refactor: simplify recent detections view HTMX targeting - Updated HTMX target ID from "recentDetections-wrapper" to "recentDetections" - Removed global loading indicator - Added custom event trigger for list refresh - Streamlined recent detections section markup --- views/dashboard.html | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/views/dashboard.html b/views/dashboard.html index ba51642b..d87641e2 100644 --- a/views/dashboard.html +++ b/views/dashboard.html @@ -49,8 +49,7 @@