Files
silo/internal/api/web.go
Forbes 73e6b813f4 feat: add component audit tool and category properties in create form
- New /audit page with completeness scoring engine
- Weighted scoring by sourcing type (purchased vs manufactured)
- Batch DB queries for items+properties, BOM existence, project codes
- API endpoints: GET /api/audit/completeness, GET /api/audit/completeness/{pn}
- Audit UI: tier summary bar, filterable table, split-panel inline editing
- Create item form now shows category-specific property fields on category select
- Properties collected and submitted with item creation
2026-02-01 10:41:57 -06:00

119 lines
3.1 KiB
Go

package api
import (
"embed"
"html/template"
"net/http"
"github.com/justinas/nosurf"
"github.com/kindredsystems/silo/internal/auth"
"github.com/rs/zerolog"
)
//go:embed templates/*.html
var templatesFS embed.FS
// WebHandler serves HTML pages.
type WebHandler struct {
templates *template.Template
logger zerolog.Logger
server *Server
}
// NewWebHandler creates a new web handler.
func NewWebHandler(logger zerolog.Logger, server *Server) (*WebHandler, error) {
tmpl, err := template.ParseFS(templatesFS, "templates/*.html")
if err != nil {
return nil, err
}
wh := &WebHandler{
templates: tmpl,
logger: logger,
server: server,
}
// Store reference on server for auth handlers to use templates
server.webHandler = wh
return wh, nil
}
// PageData holds data for page rendering.
type PageData struct {
Title string
Page string
Data any
User *auth.User
CSRFToken string
}
// HandleIndex serves the main items page.
func (h *WebHandler) HandleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
data := PageData{
Title: "Items",
Page: "items",
User: auth.UserFromContext(r.Context()),
CSRFToken: nosurf.Token(r),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.ExecuteTemplate(w, "base.html", data); err != nil {
h.logger.Error().Err(err).Msg("failed to render template")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
// HandleProjectsPage serves the projects page.
func (h *WebHandler) HandleProjectsPage(w http.ResponseWriter, r *http.Request) {
data := PageData{
Title: "Projects",
Page: "projects",
User: auth.UserFromContext(r.Context()),
CSRFToken: nosurf.Token(r),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.ExecuteTemplate(w, "base.html", data); err != nil {
h.logger.Error().Err(err).Msg("failed to render template")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
// HandleSchemasPage serves the schemas page.
func (h *WebHandler) HandleSchemasPage(w http.ResponseWriter, r *http.Request) {
data := PageData{
Title: "Schemas",
Page: "schemas",
User: auth.UserFromContext(r.Context()),
CSRFToken: nosurf.Token(r),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.ExecuteTemplate(w, "base.html", data); err != nil {
h.logger.Error().Err(err).Msg("failed to render template")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
// HandleAuditPage serves the component audit page.
func (h *WebHandler) HandleAuditPage(w http.ResponseWriter, r *http.Request) {
data := PageData{
Title: "Audit",
Page: "audit",
User: auth.UserFromContext(r.Context()),
CSRFToken: nosurf.Token(r),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := h.templates.ExecuteTemplate(w, "base.html", data); err != nil {
h.logger.Error().Err(err).Msg("failed to render template")
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}