diff --git a/.gitignore b/.gitignore index 72d2520..71d4cb0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ tmp/ static/css/output.css internal/view/**/*_templ.go internal/view/**/*_templ.txt +oslstats # Database backups (compressed) backups/*.sql.gz diff --git a/cmd/oslstats/httpserver.go b/cmd/oslstats/httpserver.go index b01c16e..e37789e 100644 --- a/cmd/oslstats/httpserver.go +++ b/cmd/oslstats/httpserver.go @@ -35,7 +35,8 @@ func setupHttpServer( ignoredPaths := []string{ "/static/css/output.css", "/static/favicon.ico", - "/static/js/popups.js", + "/static/js/toasts.js", + "/static/js/copytoclipboard.js", "/static/js/theme.js", "/static/vendored/htmx@2.0.8.min.js", "/static/vendored/htmx-ext-ws.min.js", diff --git a/internal/handlers/errorpage.go b/internal/handlers/errorpage.go index d6e4a25..53cb172 100644 --- a/internal/handlers/errorpage.go +++ b/internal/handlers/errorpage.go @@ -7,25 +7,6 @@ import ( "git.haelnorr.com/h/oslstats/internal/view/page" ) -// func ErrorPage( -// error hws.HWSError, -// ) (hws.ErrorPage, error) { -// messages := map[int]string{ -// 400: "The request you made was malformed or unexpected.", -// 401: "You need to login to view this page.", -// 403: "You do not have permission to view this page.", -// 404: "The page or resource you have requested does not exist.", -// 500: `An error occured on the server. Please try again, and if this -// continues to happen contact an administrator.`, -// 503: "The server is currently down for maintenance and should be back soon. =)", -// } -// msg, exists := messages[error.StatusCode] -// if !exists { -// return nil, errors.New("No valid message for the given code") -// } -// return page.Error(error.StatusCode, http.StatusText(error.StatusCode), msg), nil -// } - func ErrorPage(hwsError hws.HWSError) (hws.ErrorPage, error) { // Determine if this status code should show technical details showDetails := shouldShowDetails(hwsError.StatusCode) @@ -40,7 +21,7 @@ func ErrorPage(hwsError hws.HWSError) (hws.ErrorPage, error) { // Get technical details if applicable var details string if showDetails && hwsError.Error != nil { - details = hwsError.Error.Error() + details = formatErrorDetails(hwsError.Error) } // Render appropriate template @@ -63,7 +44,7 @@ func ErrorPage(hwsError hws.HWSError) (hws.ErrorPage, error) { // shouldShowDetails determines if a status code should display technical details func shouldShowDetails(statusCode int) bool { switch statusCode { - case 400, 500, 503: // Bad Request, Internal Server Error, Service Unavailable + case 400, 418, 500, 503: // Bad Request, Internal Server Error, Service Unavailable return true case 401, 403, 404: // Unauthorized, Forbidden, Not Found return false @@ -80,6 +61,7 @@ func getDefaultMessage(statusCode int) string { 401: "You need to login to view this page.", 403: "You do not have permission to view this page.", 404: "The page or resource you have requested does not exist.", + 418: "I'm a teapot!", 500: `An error occurred on the server. Please try again, and if this continues to happen contact an administrator.`, 503: "The server is currently down for maintenance and should be back soon. =)", diff --git a/internal/handlers/errors.go b/internal/handlers/errors.go index 4e26611..bad79d6 100644 --- a/internal/handlers/errors.go +++ b/internal/handlers/errors.go @@ -1,6 +1,7 @@ package handlers import ( + "encoding/json" "fmt" "net/http" @@ -107,3 +108,50 @@ func throwNotFound( err := errors.New("Resource not found") throwError(s, w, r, http.StatusNotFound, msg, err, "debug") } + +// ErrorDetails contains structured error information for WebSocket error modals +type ErrorDetails struct { + Code int `json:"code"` + Stacktrace string `json:"stacktrace"` +} + +// formatErrorDetails extracts and formats error details from wrapped errors +func formatErrorDetails(err error) string { + if err == nil { + return "" + } + // Use %+v format to get stack trace from github.com/pkg/errors + return fmt.Sprintf("%+v", err) +} + +// SerializeErrorDetails creates a JSON string with code and stacktrace +// This is exported so it can be used when creating error notifications +func SerializeErrorDetails(code int, err error) string { + details := ErrorDetails{ + Code: code, + Stacktrace: formatErrorDetails(err), + } + jsonData, jsonErr := json.Marshal(details) + if jsonErr != nil { + // Fallback if JSON encoding fails + return fmt.Sprintf(`{"code":%d,"stacktrace":"Failed to serialize error"}`, code) + } + return string(jsonData) +} + +// parseErrorDetails extracts code and stacktrace from JSON Details field +// Returns (code, stacktrace). If parsing fails, returns (500, original details string) +func parseErrorDetails(details string) (int, string) { + if details == "" { + return 500, "" + } + + var errDetails ErrorDetails + err := json.Unmarshal([]byte(details), &errDetails) + if err != nil { + // Not JSON or malformed - treat as plain stacktrace with default code + return 500, details + } + + return errDetails.Code, errDetails.Stacktrace +} diff --git a/internal/handlers/notifications.go b/internal/handlers/notifications.go deleted file mode 100644 index 3d17197..0000000 --- a/internal/handlers/notifications.go +++ /dev/null @@ -1,141 +0,0 @@ -package handlers - -import ( - "encoding/json" - "fmt" - "net/http" - - "git.haelnorr.com/h/golib/hws" -) - -// NotificationType defines the type of notification -type NotificationType string - -const ( - NotificationSuccess NotificationType = "success" - NotificationWarning NotificationType = "warning" - NotificationInfo NotificationType = "info" -) - -// Notification represents a toast notification (success, warning, info) -type Notification struct { - Type NotificationType `json:"type"` - Title string `json:"title"` - Message string `json:"message"` -} - -// ErrorModal represents a full-screen error modal (500, 503) -type ErrorModal struct { - Code int `json:"code"` - Title string `json:"title"` - Message string `json:"message"` - Details string `json:"details"` -} - -// setHXTrigger sets the HX-Trigger header with JSON-encoded data -func setHXTrigger(w http.ResponseWriter, event string, data any) { - payload := map[string]any{ - event: data, - } - - jsonData, err := json.Marshal(payload) - if err != nil { - // Fallback if JSON encoding fails - w.Header().Set("HX-Trigger", event) - return - } - - w.Header().Set("HX-Trigger", string(jsonData)) -} - -// formatErrorDetails extracts and formats error details from wrapped errors -func formatErrorDetails(err error) string { - if err == nil { - return "" - } - - // Use %+v format to get stack trace from github.com/pkg/errors - return fmt.Sprintf("%+v", err) -} - -// notifyToast sends a toast notification via HX-Trigger header -func notifyToast( - w http.ResponseWriter, - notifType NotificationType, - title string, - message string, -) { - notification := Notification{ - Type: notifType, - Title: title, - Message: message, - } - - setHXTrigger(w, "showNotification", notification) -} - -// notifyErrorModal sends a full-screen error modal via HX-Trigger header -func notifyErrorModal( - s *hws.Server, - w http.ResponseWriter, - statusCode int, - title string, - message string, - err error, -) { - modal := ErrorModal{ - Code: statusCode, - Title: title, - Message: message, - Details: formatErrorDetails(err), - } - - // Log the error - s.LogError(hws.HWSError{ - StatusCode: statusCode, - Message: message, - Error: err, - Level: hws.ErrorERROR, - }) - - // Set response status - w.WriteHeader(statusCode) - - // Send notification via HX-Trigger - setHXTrigger(w, "showErrorModal", modal) -} - -// notifySuccess sends a success toast notification -func notifySuccess(w http.ResponseWriter, title string, message string) { - notifyToast(w, NotificationSuccess, title, message) -} - -// notifyWarning sends a warning toast notification -func notifyWarning(w http.ResponseWriter, title string, message string) { - notifyToast(w, NotificationWarning, title, message) -} - -// notifyInfo sends an info toast notification -func notifyInfo(w http.ResponseWriter, title string, message string) { - notifyToast(w, NotificationInfo, title, message) -} - -// notifyInternalServiceError sends a 500 error modal -func notifyInternalServiceError( - s *hws.Server, - w http.ResponseWriter, - message string, - err error, -) { - notifyErrorModal(s, w, 500, "Internal Server Error", message, err) -} - -// notifyServiceUnavailable sends a 503 error modal -func notifyServiceUnavailable( - s *hws.Server, - w http.ResponseWriter, - message string, - err error, -) { - notifyErrorModal(s, w, 503, "Service Unavailable", message, err) -} diff --git a/internal/handlers/notifswebsocket.go b/internal/handlers/notifswebsocket.go index 694fde0..9e5caea 100644 --- a/internal/handlers/notifswebsocket.go +++ b/internal/handlers/notifswebsocket.go @@ -104,7 +104,9 @@ func notifyLoop(ctx context.Context, c *hws.Client, ws *websocket.Conn) error { case notify.LevelWarn: err = popup.Toast(nt, count, 10000).Render(ctx, w) case notify.LevelError: - // do error modal + // Parse error code and stacktrace from Details field + code, stacktrace := parseErrorDetails(nt.Details) + err = popup.ErrorModalWS(code, stacktrace, nt, count).Render(ctx, w) default: err = popup.Toast(nt, count, 6000).Render(ctx, w) } diff --git a/internal/handlers/test.go b/internal/handlers/test.go index f9091e9..d26c094 100644 --- a/internal/handlers/test.go +++ b/internal/handlers/test.go @@ -3,11 +3,11 @@ package handlers import ( "net/http" - "git.haelnorr.com/h/oslstats/internal/view/page" "github.com/pkg/errors" "git.haelnorr.com/h/golib/hws" "git.haelnorr.com/h/golib/notify" + "git.haelnorr.com/h/oslstats/internal/view/page" ) // Handles responses to the / path. Also serves a 404 Page for paths that @@ -15,7 +15,14 @@ import ( func NotifyTester(server *hws.Server) http.Handler { return http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { + testErr := errors.New("This is a stack trace. No really i swear. Just pretend ok? Thanks") if r.Method == "GET" { + // page, _ := ErrorPage(hws.HWSError{ + // StatusCode: http.StatusTeapot, + // Message: "This error has been rendered as a test", + // Error: testErr, + // }) + // page.Render(r.Context(), w) page.Test().Render(r.Context(), w) } else { r.ParseForm() @@ -27,16 +34,20 @@ func NotifyTester(server *hws.Server) http.Handler { "warn": notify.LevelWarn, "error": notify.LevelError, }[r.Form.Get("type")] - error := errors.New("This is a stack trace. No really i swear. Just pretend ok? Thanks") message := r.Form.Get("message") nt := notify.Notification{ Target: notify.Target(target), Title: title, Message: message, Level: level, - Details: error.Error(), } - if target == "" { + + // For error level, serialize error details with code + if level == notify.LevelError { + nt.Details = SerializeErrorDetails(500, testErr) + } + + if target == "all" { server.NotifyAll(nt) } else { server.NotifySub(nt) @@ -45,7 +56,3 @@ func NotifyTester(server *hws.Server) http.Handler { }, ) } - -func notifyInfoWS() { - -} diff --git a/internal/view/component/popup/errorModal.templ b/internal/view/component/popup/errorModal.templ deleted file mode 100644 index 5d67e49..0000000 --- a/internal/view/component/popup/errorModal.templ +++ /dev/null @@ -1,126 +0,0 @@ -package popup - -// ErrorModal displays a full-screen modal for critical errors (500, 503) -templ ErrorModal() { -
+ { nt.Title } +
+ +{ nt.Message }
+ + if stacktrace != "" { +{ stacktrace }
+ - An error occured on the server. Please try again later, - or contact an administrator -
-+
{ nt.Title }
@@ -97,11 +103,11 @@ templ Toast(nt notify.Notification, id, duration int) {