Files
bookhoard/internal/router/frontend.go
T
john-okeefe d9356f0f85 feat: enhance health check endpoint with detailed error info and scan status
- Return actual database error message instead of generic "unavailable"
- Add scan status information to healthy response (scan_in_progress, active_jobs)
- Maintain backward compatibility while providing more actionable diagnostics
- Use map[string]interface{} to support nested scan status structure

These changes improve observability by providing administrators with
specific error messages and scan status information, making it easier
to diagnose issues and monitor system state.
2026-03-05 19:35:04 -05:00

853 lines
26 KiB
Go

package router
import (
"bytes"
"context"
"log"
"net/http"
"time"
"bookhoard/internal/database"
"bookhoard/internal/handlers"
"bookhoard/internal/services"
"bookhoard/internal/utils"
"bookhoard/templates"
"github.com/golang-jwt/jwt/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/labstack/echo/v4"
"github.com/google/uuid"
)
func renderErrorPage(c echo.Context, message string, errorType string) error {
var buf bytes.Buffer
err := templates.ErrorPage(message, errorType).Render(c.Request().Context(), &buf)
if err != nil {
log.Printf("renderErrorPage failed to render template: %v", err)
return c.HTML(http.StatusInternalServerError, "Internal server error")
}
return c.HTML(http.StatusInternalServerError, buf.String())
}
func registerFrontendRoutes(cfg *Config) {
e := cfg.Echo
// ============================================================================
// FRONTEND ROUTES - DO NOT DELETE
// These routes serve Server-Side Rendered (SSR) HTML pages for the web UI.
// They are NOT API endpoints and should NOT be removed during refactors.
// All authenticated frontend routes use the jwtMiddleware to validate tokens.
// ============================================================================
// Use existing JWT middleware (sets database user object in context)
jwtMiddleware := createJWTMiddleware(cfg)
// Protected route group for API routes
protected := e.Group("/api", jwtMiddleware)
// ============================================================================
// PUBLIC FRONTEND ROUTES (No authentication required)
// ============================================================================
// Public routes for login and registration pages
e.GET("/login", func(c echo.Context) error {
var buf bytes.Buffer
sessionExpired := c.QueryParam("session") == "expired"
deleted := c.QueryParam("deleted") == "true"
err := templates.Login(sessionExpired, deleted).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
e.GET("/register", func(c echo.Context) error {
var buf bytes.Buffer
err := templates.Register().Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Root route - landing page with smart login detection
e.GET("/", func(c echo.Context) error {
var buf bytes.Buffer
var err error
tokenString := c.Request().Header.Get("Authorization")
if tokenString != "" && len(tokenString) > 7 && tokenString[:7] == "Bearer " {
tokenString = tokenString[7:]
} else {
cookie, err := c.Cookie("token")
if err == nil {
tokenString = cookie.Value
}
}
loggedIn := false
if tokenString != "" {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(cfg.Cfg.JWTSecret), nil
})
loggedIn = err == nil && token.Valid
}
err = templates.Index(loggedIn).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// ============================================================================
// PROTECTED FRONTEND ROUTES (Authentication required)
// ============================================================================
// Protected frontend routes (no /api prefix)
frontendProtected := e.Group("", jwtMiddleware, ensureUserExistsMiddleware(cfg))
// Helper to extract text from pgtype.Text
getText := func(t pgtype.Text) string {
if t.Valid {
return t.String
}
return ""
}
// Dashboard page
frontendProtected.GET("/dashboard", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var errorMsg string
libraryID := c.QueryParam("library_id")
if libraryID == "" {
userUUID, _ := uuid.Parse(user.ID)
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
if err == nil && len(libraries) > 0 {
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
libraryID = libUUID.String()
}
}
libUUID, _ := uuid.Parse(libraryID)
userUUID, _ := uuid.Parse(user.ID)
prefs, err := cfg.DashboardService.GetDashboardPreferences(c.Request().Context(), userUUID, libUUID)
if err != nil {
log.Printf("GetDashboardPreferences failed: %v", err)
prefs = database.UserDashboardPreferences{
HiddenCollections: []string{},
CollectionOrder: []string{},
ItemsPerSection: pgtype.Int4{Int32: 20, Valid: true},
}
}
limit := 20
if prefs.ItemsPerSection.Int32 > 0 {
limit = int(prefs.ItemsPerSection.Int32)
}
// Get ALL sections (unfiltered) for the modal
allSections, err := cfg.DashboardService.GetDashboardSections(
c.Request().Context(),
userUUID,
libUUID,
limit,
prefs.CollectionOrder,
[]string{}, // No filtering - get all sections
)
if err != nil {
log.Printf("Dashboard sections query failed: %v", err)
allSections = []services.DashboardSection{}
errorMsg = "Error loading dashboard"
}
// Get only visible sections for the dashboard display
visibleSections := cfg.DashboardService.FilterHiddenCollections(allSections, prefs.HiddenCollections)
userUUID2, _ := uuid.Parse(user.ID)
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID2))
if err != nil {
log.Printf("GetUserVisibleLibraries failed: %v", err)
libraries = []database.GetUserVisibleLibrariesRow{}
if errorMsg == "" {
errorMsg = "Error loading libraries"
}
}
libData := make([]templates.LibraryData, len(libraries))
for i, lib := range libraries {
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
libData[i] = templates.LibraryData{
ID: libUUID.String(),
Name: lib.Name,
Description: getText(lib.Description),
TypeName: lib.TypeName,
}
}
sectionData := handlers.BuildSections(visibleSections, libraryID)
allSectionsData := handlers.BuildSections(allSections, libraryID)
var buf bytes.Buffer
err = templates.Dashboard(user, sectionData, allSectionsData, libData, libraryID, prefs.HiddenCollections, limit, errorMsg).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Collections page
frontendProtected.GET("/collections", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var errorMsg string
var collections []database.Collections
collections, err = cfg.CollectionHandler.GetCollectionsData(c)
if err != nil {
log.Printf("GetCollectionsData failed: %v", err)
collections = []database.Collections{}
errorMsg = "Error loading collections"
}
colData := make([]templates.CollectionData, len(collections))
for i, col := range collections {
colData[i] = templates.CollectionData{
ID: uuid.UUID(col.ID.Bytes).String(),
Name: col.Name,
Description: getText(col.Description),
Color: getText(col.Color),
Icon: getText(col.Icon),
}
}
var buf bytes.Buffer
err = templates.Collection(user, colData, errorMsg).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Collection create modal
frontendProtected.GET("/collections/create-modal", func(c echo.Context) error {
var buf bytes.Buffer
err := templates.CollectionModal(templates.CollectionData{}).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Collection edit modal
frontendProtected.GET("/collections/:id/edit-modal", func(c echo.Context) error {
// Parse collection ID
collectionID := c.Param("id")
collUUID, err := uuid.Parse(collectionID)
if err != nil {
return c.HTML(http.StatusBadRequest, "<div>Invalid collection ID</div>")
}
// Fetch collection data
collection, err := cfg.Queries.GetCollection(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true})
if err != nil {
return c.HTML(http.StatusNotFound, "<div>Collection not found</div>")
}
// Convert to template type
colData := templates.CollectionData{
ID: uuid.UUID(collection.ID.Bytes).String(),
Name: collection.Name,
Description: getText(collection.Description),
Color: getText(collection.Color),
Icon: getText(collection.Icon),
}
// Ensure default color if empty
if colData.Color == "" {
colData.Color = "blue"
}
var buf bytes.Buffer
err = templates.CollectionModal(colData).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Restore system collection modal
frontendProtected.GET("/collections/restore-modal", func(c echo.Context) error {
var buf bytes.Buffer
err := templates.RestoreSystemCollectionModal().Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Collection detail page (works for both system and user collections)
frontendProtected.GET("/collections/:id", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
// Parse collection ID from URL
collectionID := c.Param("id")
collUUID, err := uuid.Parse(collectionID)
if err != nil {
return renderErrorPage(c, "Invalid collection ID", "invalid_id")
}
// Fetch collection details
collection, err := cfg.Queries.GetCollection(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true})
if err != nil {
if err.Error() == "no rows in result set" {
return renderErrorPage(c, "Collection not found", "not_found")
}
return renderErrorPage(c, "Error loading collection", "collection_load_error")
}
// Fetch books in collection
userUUID, _ := uuid.Parse(user.ID)
var books []handlers.BookInfo
if collection.QueryType.Valid && collection.QueryType.String != "" {
// System collection - use query type
// System collection - need library_id for system collections
// Get library_id from query param or default to user's first library
libraryID := c.QueryParam("library_id")
if libraryID == "" {
libraries, err := cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
if err == nil && len(libraries) > 0 {
libUUID, _ := uuid.FromBytes(libraries[0].ID.Bytes[0:16])
libraryID = libUUID.String()
}
}
libUUID, _ := uuid.Parse(libraryID)
dashboardSvc := services.NewDashboardService(cfg.Queries)
sections, err := dashboardSvc.GetDashboardSections(c.Request().Context(), userUUID, libUUID, 1000, []string{}, []string{})
if err != nil {
return renderErrorPage(c, "Error loading books", "books_load_error")
}
// Find the matching section and convert items
for _, section := range sections {
if section.CollectionID.String() == collectionID {
// Convert []database.MediaItems to []handlers.BookInfo
bookCards := make([]handlers.BookInfo, len(section.Items))
for i, item := range section.Items {
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
bookCards[i] = handlers.BookInfo{
MediaItemID: itemUUID.String(),
Title: item.Title,
Author: getText(item.Author),
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
}
}
books = bookCards
break
}
}
} else {
// User collection - check if library_id filter is present
libraryID := c.QueryParam("library_id")
if libraryID != "" {
// Filter by library - reuse dashboard query
libUUID, err := uuid.Parse(libraryID)
if err != nil {
return renderErrorPage(c, "Invalid library ID", "invalid_library_id")
}
// Use GetCollectionItemsForDashboard for library-filtered results
collItems, err := cfg.Queries.GetCollectionItemsForDashboard(c.Request().Context(),
database.GetCollectionItemsForDashboardParams{
CollectionID: pgtype.UUID{Bytes: collUUID, Valid: true},
LibraryID: pgtype.UUID{Bytes: libUUID, Valid: true},
Limit: 1000,
})
if err != nil {
books = []handlers.BookInfo{}
} else {
// Convert to BookInfo format (non-excluded only)
var validItems []database.GetCollectionItemsForDashboardRow
for _, item := range collItems {
if !item.Excluded.Valid || !item.Excluded.Bool {
validItems = append(validItems, item)
}
}
bookCards := make([]handlers.BookInfo, len(validItems))
for i, item := range validItems {
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
bookCards[i] = handlers.BookInfo{
MediaItemID: itemUUID.String(),
Title: item.Title,
Author: getText(item.Author),
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
}
}
books = bookCards
}
} else {
// No library filter - show all books in collection
collItems, err := cfg.Queries.GetCollectionItems(c.Request().Context(), pgtype.UUID{Bytes: collUUID, Valid: true})
if err != nil {
books = []handlers.BookInfo{}
}
// Convert to BookInfo format
bookCards := make([]handlers.BookInfo, len(collItems))
for i, item := range collItems {
itemUUID, _ := uuid.FromBytes(item.MediaItemID.Bytes[0:16])
bookCards[i] = handlers.BookInfo{
MediaItemID: itemUUID.String(),
Title: item.Title,
Author: getText(item.Author),
CoverImagePath: utils.ResolveMediaURL(item.LibraryID, item.CoverImagePath),
}
}
books = bookCards
}
}
// Build collection data
colData := templates.CollectionData{
ID: collectionID,
Name: collection.Name,
Description: collection.Description.String,
Color: collection.Color.String,
Icon: collection.Icon.String,
}
// Get library_id from query params for template
libraryID := c.QueryParam("library_id")
// Render the CollectionDetail template
var buf bytes.Buffer
err = templates.CollectionDetail(user, colData, books, libraryID).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Custom Section Builder page
frontendProtected.GET("/custom-section", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var errorMsg string
var libraries []database.GetUserVisibleLibrariesRow
userUUID, _ := uuid.Parse(user.ID)
libraries, err = cfg.Queries.GetUserVisibleLibraries(c.Request().Context(), uuidToPGType(userUUID))
if err != nil {
log.Printf("GetUserVisibleLibraries failed: %v", err)
libraries = []database.GetUserVisibleLibrariesRow{}
errorMsg = "Error loading libraries"
}
libData := make([]templates.LibraryData, len(libraries))
for i, lib := range libraries {
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
libData[i] = templates.LibraryData{
ID: libUUID.String(),
Name: lib.Name,
Description: getText(lib.Description),
TypeName: lib.TypeName,
}
}
var buf bytes.Buffer
err = templates.CustomSectionBuilder(user, libData, errorMsg).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Progress page
frontendProtected.GET("/progress", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var errorMsg string
var progressData []handlers.ProgressWithMedia
progressData, err = cfg.ScannerHandler.GetAllProgressData(c)
if err != nil {
log.Printf("GetAllProgressData failed: %v", err)
progressData = []handlers.ProgressWithMedia{}
errorMsg = "Error loading progress"
}
var buf bytes.Buffer
err = templates.Progress(user, progressData, errorMsg).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Devices page
frontendProtected.GET("/devices", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var errorMsg string
var devices []handlers.DeviceInfo
var pendingList []templates.PendingRegistrationData
devices, err = cfg.DeviceHandler.GetDevicesData(c)
if err != nil {
log.Printf("GetDevicesData failed: %v", err)
devices = []handlers.DeviceInfo{}
errorMsg = "Error loading devices"
}
pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
if err != nil {
log.Printf("GetPendingRegistrationsData failed: %v", err)
pendingList = []templates.PendingRegistrationData{}
if errorMsg == "" {
errorMsg = "Error loading pending registrations"
}
} else {
pendingList = convertPending(pendingMaps)
}
var buf bytes.Buffer
err = templates.Devices(user, devices, pendingList, errorMsg, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Conflicts page
frontendProtected.GET("/conflicts", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var errorMsg string
var conflictsData []handlers.ConflictDetailResponse
var total, unresolved int
conflictsData, total, unresolved, err = cfg.ConflictHandler.GetConflictsData(c)
if err != nil {
log.Printf("GetConflictsData failed: %v", err)
conflictsData = []handlers.ConflictDetailResponse{}
total = 0
unresolved = 0
errorMsg = "Error loading conflicts"
}
var buf bytes.Buffer
err = templates.Conflicts(user, conflictsData, total, unresolved, errorMsg).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Analytics page
frontendProtected.GET("/analytics", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var buf bytes.Buffer
err = templates.Analytics(user).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// Profile page (all users)
frontendProtected.GET("/profile", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var buf bytes.Buffer
err = templates.Profile(user).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// ============================================================================
// ADMIN FRONTEND ROUTES
// ============================================================================
// Admin routes
frontendProtected.GET("/admin", handlers.AdminMiddleware(func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var buf bytes.Buffer
err = templates.Admin(user).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
}))
frontendProtected.GET("/admin/", handlers.AdminMiddleware(func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var buf bytes.Buffer
err = templates.Admin(user).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
}))
frontendProtected.GET("/admin/library", handlers.AdminMiddleware(func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
// Fetch all libraries (admin-only)
libraries, err := cfg.LibraryHandler.ListLibrariesData(c.Request().Context())
if err != nil {
log.Printf("ListLibrariesData failed: %v", err)
libraries = []database.ListLibrariesRow{}
}
// Convert to template LibraryData
libData := make([]templates.LibraryData, len(libraries))
for i, lib := range libraries {
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
libData[i] = templates.LibraryData{
ID: libUUID.String(),
Name: lib.Name,
Description: getText(lib.Description),
TypeName: lib.TypeName,
}
}
// Fetch all users for visibility management
users, err := cfg.Queries.ListUsers(c.Request().Context())
if err != nil {
log.Printf("ListUsers failed: %v", err)
users = []database.ListUsersRow{}
}
// Convert to template User types
userData := make([]templates.User, len(users))
for i, u := range users {
userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16])
userData[i] = templates.User{
ID: userUUID.String(),
Username: u.Username,
Email: u.Email,
Role: u.Role,
}
}
var buf bytes.Buffer
err = templates.AdminLibrary(user, libData, userData).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
}))
// Admin users page
frontendProtected.GET("/admin/users", handlers.AdminMiddleware(func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
// Fetch all users
users, err := cfg.Queries.ListUsers(c.Request().Context())
if err != nil {
return renderErrorPage(c, "Error loading users", "users_load_error")
}
// Count admins for UI protection
adminCount := 0
for _, u := range users {
if u.Role == "admin" {
adminCount++
}
}
// Convert to template users
templateUsers := make([]templates.User, len(users))
for i, u := range users {
templateUsers[i] = templates.User{
ID: uuid.UUID(u.ID.Bytes).String(),
Username: u.Username,
Email: u.Email,
Role: u.Role,
Theme: getText(u.Theme),
FirstName: getText(u.FirstName),
LastName: getText(u.LastName),
CreatedAt: u.CreatedAt.Time,
}
}
var buf bytes.Buffer
err = templates.AdminUsers(user, templateUsers, adminCount).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
}))
// Admin: Get profile modal for editing user
frontendProtected.GET("/admin/users/:id/profile-modal", handlers.AdminMiddleware(func(c echo.Context) error {
// Get target user ID from URL
targetUserID := c.Param("id")
parsedUUID, err := uuid.Parse(targetUserID)
if err != nil {
return c.HTML(http.StatusBadRequest, "<div>Invalid user ID</div>")
}
// Fetch target user
targetUser, err := cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(parsedUUID))
if err != nil {
return c.HTML(http.StatusNotFound, "<div>User not found</div>")
}
// Convert to template user
templateUser := templates.User{
ID: uuid.UUID(targetUser.ID.Bytes).String(),
Username: targetUser.Username,
Email: targetUser.Email,
Role: targetUser.Role,
Theme: getText(targetUser.Theme),
FirstName: getText(targetUser.FirstName),
LastName: getText(targetUser.LastName),
CreatedAt: targetUser.CreatedAt.Time,
}
// Render modal
var buf bytes.Buffer
err = templates.ProfileModal(templateUser).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
}))
// ============================================================================
// LEGACY API ROUTES (for backward compatibility)
// ============================================================================
// Keep legacy routes under /api for existing API consumers
protected.GET("/devices-page", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var errorMsg string
var devices []handlers.DeviceInfo
var pendingList []templates.PendingRegistrationData
devices, err = cfg.DeviceHandler.GetDevicesData(c)
if err != nil {
log.Printf("GetDevicesData failed: %v", err)
devices = []handlers.DeviceInfo{}
errorMsg = "Error loading devices"
}
pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
if err != nil {
log.Printf("GetPendingRegistrationsData failed: %v", err)
pendingList = []templates.PendingRegistrationData{}
if errorMsg == "" {
errorMsg = "Error loading pending registrations"
}
} else {
pendingList = convertPending(pendingMaps)
}
var buf bytes.Buffer
err = templates.Devices(user, devices, pendingList, errorMsg, cfg.Cfg.BaseURL).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
protected.GET("/conflicts-page", func(c echo.Context) error {
user, err := getTemplateUserWithTheme(c, cfg)
if err != nil {
return renderErrorPage(c, "Error loading user", "user_load_error")
}
var errorMsg string
var conflictsData []handlers.ConflictDetailResponse
var total, unresolved int
conflictsData, total, unresolved, err = cfg.ConflictHandler.GetConflictsData(c)
if err != nil {
log.Printf("GetConflictsData failed: %v", err)
conflictsData = []handlers.ConflictDetailResponse{}
total = 0
unresolved = 0
errorMsg = "Error loading conflicts"
}
var buf bytes.Buffer
err = templates.Conflicts(user, conflictsData, total, unresolved, errorMsg).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
return c.HTML(http.StatusOK, buf.String())
})
// ============================================================================
// HEALTH CHECK
// ============================================================================
// Health check
e.GET("/health", func(c echo.Context) error {
ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)
defer cancel()
if err := pingDB(cfg, ctx); err != nil {
return c.JSON(http.StatusServiceUnavailable, map[string]string{
"status": "unhealthy",
"error": err.Error(),
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"status": "healthy",
"database": "connected",
"scan": map[string]interface{}{
"scan_in_progress": false, // Would check worker results
"active_jobs": 0, // Would count running jobs
},
})
})
}