diff --git a/docs/REPOSITORY_STATUS.md b/docs/REPOSITORY_STATUS.md new file mode 100644 index 0000000..0f81564 --- /dev/null +++ b/docs/REPOSITORY_STATUS.md @@ -0,0 +1,285 @@ +# Repository Status Report + +**Generated:** 2026-01-31 +**Branch:** main +**Last Build:** `go build ./...` and `go vet ./...` pass clean + +--- + +## Codebase Summary + +| Category | Lines | Files | +|----------|-------|-------| +| Go source | ~6,644 | 20 | +| HTML templates | ~4,923 | 4 | +| Python (FreeCAD) | ~2,499 | 7 | +| SQL migrations | ~464 | 8 | +| **Total** | **~14,730** | **39** | + +--- + +## Architecture + +``` +cmd/ + silo/ CLI client (313 lines) + silod/ API server (126 lines) +internal/ + api/ HTTP handlers, routes, middleware, templates (3,491 Go + 4,923 HTML) + config/ YAML config loading (132 lines) + db/ PostgreSQL repositories (1,634 lines) + migration/ Property migration framework (211 lines) + odoo/ Odoo ERP integration stubs (201 lines) + partnum/ Part number generator (180 lines) + schema/ YAML schema parser (235 lines) + storage/ MinIO S3 client (121 lines) +pkg/freecad/ FreeCAD workbench plugin (2,499 Python) +migrations/ Database DDL (8 files) +``` + +### Key Dependencies + +- `go-chi/chi/v5` -- HTTP router +- `jackc/pgx/v5` -- PostgreSQL driver +- `minio/minio-go/v7` -- S3-compatible storage +- `rs/zerolog` -- Structured logging +- `sahilm/fuzzy` -- Fuzzy text matching +- `gopkg.in/yaml.v3` -- YAML parsing + +--- + +## API Surface (38 Routes) + +### Web UI +| Method | Path | Handler | +|--------|------|---------| +| GET | `/` | Items page | +| GET | `/projects` | Projects page | +| GET | `/schemas` | Schemas page | + +### Items +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/items` | List with filtering and pagination | +| POST | `/api/items` | Create item | +| GET | `/api/items/search` | Fuzzy search | +| GET | `/api/items/export.csv` | CSV export | +| POST | `/api/items/import` | CSV import | +| GET | `/api/items/template.csv` | CSV template | +| GET | `/api/items/{partNumber}` | Get item | +| PUT | `/api/items/{partNumber}` | Update item | +| DELETE | `/api/items/{partNumber}` | Archive item | + +### Revisions +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/items/{pn}/revisions` | List revisions | +| POST | `/api/items/{pn}/revisions` | Create revision | +| GET | `/api/items/{pn}/revisions/compare` | Compare two revisions | +| GET | `/api/items/{pn}/revisions/{rev}` | Get revision | +| PATCH | `/api/items/{pn}/revisions/{rev}` | Update status/label | +| POST | `/api/items/{pn}/revisions/{rev}/rollback` | Rollback | + +### Files +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/items/{pn}/file` | Upload file | +| GET | `/api/items/{pn}/file` | Download latest | +| GET | `/api/items/{pn}/file/{rev}` | Download at revision | + +### BOM +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/items/{pn}/bom` | List children | +| POST | `/api/items/{pn}/bom` | Add child | +| GET | `/api/items/{pn}/bom/expanded` | Multi-level BOM | +| GET | `/api/items/{pn}/bom/where-used` | Where-used lookup | +| GET | `/api/items/{pn}/bom/export.csv` | BOM CSV export | +| POST | `/api/items/{pn}/bom/import` | BOM CSV import | +| PUT | `/api/items/{pn}/bom/{child}` | Update quantity/ref | +| DELETE | `/api/items/{pn}/bom/{child}` | Remove child | + +### Projects +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/projects` | List projects | +| POST | `/api/projects` | Create project | +| GET | `/api/projects/{code}` | Get project | +| PUT | `/api/projects/{code}` | Update project | +| DELETE | `/api/projects/{code}` | Delete project | +| GET | `/api/projects/{code}/items` | Items in project | + +### Item-Project Associations +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/items/{pn}/projects` | Item's projects | +| POST | `/api/items/{pn}/projects` | Tag item to project | +| DELETE | `/api/items/{pn}/projects/{code}` | Remove tag | + +### Schemas +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/schemas` | List schemas | +| GET | `/api/schemas/{name}` | Get schema | +| GET | `/api/schemas/{name}/properties` | Property definitions | +| POST | `/api/schemas/{name}/segments/{seg}/values` | Add enum value | +| PUT | `/api/schemas/{name}/segments/{seg}/values/{code}` | Update enum value | +| DELETE | `/api/schemas/{name}/segments/{seg}/values/{code}` | Delete enum value | + +### Odoo Integration (Stubs) +| Method | Path | Description | +|--------|------|-------------| +| GET | `/api/integrations/odoo/config` | Get config | +| PUT | `/api/integrations/odoo/config` | Update config | +| GET | `/api/integrations/odoo/sync-log` | Sync history | +| POST | `/api/integrations/odoo/test-connection` | Test connection | +| POST | `/api/integrations/odoo/sync/push/{pn}` | Push to Odoo | +| POST | `/api/integrations/odoo/sync/pull/{id}` | Pull from Odoo | + +### Other +| Method | Path | Description | +|--------|------|-------------| +| GET | `/health` | Health probe | +| GET | `/ready` | Readiness probe (DB + MinIO) | +| POST | `/api/generate-part-number` | Generate next PN | + +--- + +## Database Migrations + +| # | File | Purpose | +|---|------|---------| +| 1 | `001_initial.sql` | Core schema: items, revisions, relationships, locations, inventory, sequences, projects, schemas | +| 2 | `002_sequence_by_name.sql` | Sequence naming changes | +| 3 | `003_remove_material.sql` | Schema cleanup | +| 4 | `004_cad_sync_state.sql` | CAD synchronization tracking | +| 5 | `005_property_schema_version.sql` | Property versioning framework | +| 6 | `006_project_tags.sql` | Many-to-many project-item relationships | +| 7 | `007_revision_status.sql` | Revision status and labels | +| 8 | `008_odoo_integration.sql` | Integrations + sync_log tables | + +--- + +## Web UI Architecture + +The web UI uses server-rendered Go templates with vanilla JavaScript (no framework). + +### Items Page (`items.html`, 3718 lines) + +Infor CloudSuite-style split-panel layout: +- **Horizontal mode** (default): item list on left, tabbed detail panel on right +- **Vertical mode**: tabbed detail panel on top, item list below +- **Detail tabs**: Main, Properties, Revisions, BOM, Where Used +- **Ctrl+F** opens in-page filter overlay with fuzzy search +- **Column config**: gear icon popover, separate settings per layout mode +- **Search scope**: All / Part Number / Description toggle pills + +### Projects Page (`projects.html`, 345 lines) +- Full CRUD for project codes +- Item count per project +- Click project code to filter items page + +### Schemas Page (`schemas.html`, 689 lines) +- Schema browsing and enum value management + +### Base Template (`base.html`, 171 lines) +- 3-tab navigation: Items, Projects, Schemas +- Catppuccin Mocha dark theme + +--- + +## Feature Stubs (Not Yet Implemented) + +These are scaffolded but contain placeholder implementations. + +### Odoo ERP Integration + +All functions in `internal/odoo/` return "not yet implemented" errors: + +| File | Function | Status | +|------|----------|--------| +| `client.go:30` | `Authenticate()` | Stub | +| `client.go:41` | `SearchRead()` | Stub | +| `client.go:51` | `Create()` | Stub | +| `client.go:60` | `Write()` | Stub | +| `client.go:70` | `TestConnection()` | Stub | +| `sync.go:27` | `PushItem()` | Stub (logs, returns nil) | +| `sync.go:36` | `PullProduct()` | Stub (logs, returns nil) | + +API handlers at `odoo_handlers.go`: +- `HandleTestOdooConnection` (line 134) -- returns stub message +- `HandleOdooPush` (line 149) -- returns stub message +- `HandleOdooPull` (line 167) -- returns stub message + +Config and sync-log CRUD handlers are functional. + +### Part Number Date Segments + +`internal/partnum/generator.go:102` -- `formatDate()` returns error. Date-based segments in schemas will fail at generation time. + +### Location and Inventory APIs + +Tables exist from migration 001 (`locations`, `inventory`) but no API handlers or repository methods are implemented. The database layer has no `LocationRepository` or `InventoryRepository`. + +--- + +## Potential Issues + +### Critical + +1. **No authentication or authorization.** All API endpoints are publicly accessible. Single-user only. Adding LDAP/FreeIPA integration is required before multi-user deployment. + +2. **No file locking.** Concurrent edits to the same item or file upload can cause data races. A pessimistic locking mechanism is needed for CAD file workflows. + +3. **No unit tests.** Zero test coverage across the entire Go codebase. Regressions cannot be caught automatically. + +### High + +4. **Large monolithic template.** `items.html` is 3,718 lines with inline CSS and JavaScript. Changes risk unintended side effects. Consider extracting JavaScript into separate files or adopting a build step. + +5. **No input validation middleware.** API handlers validate some fields inline but there is no systematic validation layer. Malformed requests may produce unclear errors or unexpected behavior. + +6. **No rate limiting.** API has no request rate controls. A misbehaving client or script could overwhelm the server. + +7. **Odoo handlers reference DB repositories not wired up.** `odoo_handlers.go` calls `s.db.IntegrationRepository()` but the `DB` struct in `db/db.go` does not expose an `IntegrationRepository` method. These handlers will panic if reached with a real database operation. Currently safe because config is stored in-memory and stubs short-circuit before DB calls. + +### Medium + +8. **No pagination on fuzzy search.** `HandleFuzzySearch` loads all items matching type/project filters into memory before fuzzy matching. Large datasets will cause high memory usage. + +9. **CSV import lacks transaction rollback on partial failure.** If import fails mid-batch, already-imported items remain. + +10. **No CSRF protection.** Web UI forms submit via `fetch()` but there are no CSRF tokens. Acceptable for single-user but a risk if authentication is added. + +11. **MinIO connection not validated at startup.** The `/ready` endpoint checks MinIO, but the server starts regardless. A misconfigured MinIO will only fail on file operations. + +12. **Property migration framework exists but has no registered migrations.** `internal/migration/properties.go` defines the framework but no concrete migrations use it yet. + +--- + +## Recent Git History + +``` +8e44ed2 2026-01-29 Fix SIGSEGV: defer document open after dialog close +e2b3f12 2026-01-29 Fix API URL: only auto-append /api for bare hostnames +559f615 2026-01-29 Fix API URL handling and SSL certificate verification +f08ecc1 2026-01-29 feat(workbench): fix icon loading and add settings dialog +53b5edb 2026-01-29 update documentation and specs +5ee88a6 2026-01-26 update deploy.sh +93add05 2026-01-26 improve csv import handling +2d44b2a 2026-01-26 add free-ipa setup +``` + +--- + +## Deployment + +### Supported Methods +- **Docker Compose** -- `deployments/docker-compose.yaml` (dev), `docker-compose.prod.yaml` (prod) +- **systemd** -- `deployments/systemd/silod.service` +- **Manual** -- `go build ./cmd/silod && ./silod -config config.yaml` + +### Infrastructure Requirements +- PostgreSQL 15+ with `pg_trgm` and `uuid-ossp` extensions +- MinIO or S3-compatible storage +- Go 1.23+ for building diff --git a/go.mod b/go.mod index 5e70569..2e94f73 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/xid v1.5.0 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect golang.org/x/crypto v0.16.0 // indirect golang.org/x/net v0.19.0 // indirect diff --git a/go.sum b/go.sum index a49900e..ab59b7b 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/api/handlers.go b/internal/api/handlers.go index f664de3..8f5910e 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "github.com/go-chi/chi/v5" "github.com/kindredsystems/silo/internal/db" @@ -263,6 +264,52 @@ func (s *Server) HandleListItems(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, response) } +// HandleFuzzySearch performs fuzzy search across items. +func (s *Server) HandleFuzzySearch(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + q := r.URL.Query().Get("q") + if q == "" { + writeJSON(w, http.StatusOK, []FuzzyResult{}) + return + } + + fieldsParam := r.URL.Query().Get("fields") + var fields []string + if fieldsParam != "" { + fields = strings.Split(fieldsParam, ",") + } + + limit := 50 + if l := r.URL.Query().Get("limit"); l != "" { + if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { + limit = parsed + } + } + + // Pre-filter by type and project via SQL (no search term) + opts := db.ListOptions{ + ItemType: r.URL.Query().Get("type"), + Project: r.URL.Query().Get("project"), + Limit: 500, // reasonable upper bound for fuzzy matching + } + + items, err := s.items.List(ctx, opts) + if err != nil { + s.logger.Error().Err(err).Msg("failed to list items for fuzzy search") + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to search items") + return + } + + responses := make([]ItemResponse, len(items)) + for i, item := range items { + responses[i] = itemToResponse(item) + } + + results := FuzzySearch(q, responses, fields, limit) + writeJSON(w, http.StatusOK, results) +} + // HandleCreateItem creates a new item with generated part number. func (s *Server) HandleCreateItem(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/internal/api/odoo_handlers.go b/internal/api/odoo_handlers.go new file mode 100644 index 0000000..edd048f --- /dev/null +++ b/internal/api/odoo_handlers.go @@ -0,0 +1,185 @@ +package api + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/kindredsystems/silo/internal/db" +) + +// OdooConfigResponse represents the Odoo configuration (API key masked). +type OdooConfigResponse struct { + Enabled bool `json:"enabled"` + URL string `json:"url"` + Database string `json:"database"` + Username string `json:"username"` + APIKey string `json:"api_key"` +} + +// OdooConfigRequest represents a request to update Odoo configuration. +type OdooConfigRequest struct { + Enabled bool `json:"enabled"` + URL string `json:"url"` + Database string `json:"database"` + Username string `json:"username"` + APIKey string `json:"api_key,omitempty"` +} + +// HandleGetOdooConfig returns the current Odoo integration configuration. +func (s *Server) HandleGetOdooConfig(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + repo := db.NewIntegrationRepository(s.db) + integration, err := repo.GetByName(ctx, "odoo") + if err != nil { + s.logger.Error().Err(err).Msg("failed to get odoo config") + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to get Odoo config") + return + } + + if integration == nil { + writeJSON(w, http.StatusOK, OdooConfigResponse{}) + return + } + + resp := OdooConfigResponse{ + Enabled: integration.Enabled, + URL: getString(integration.Config, "url"), + Database: getString(integration.Config, "database"), + Username: getString(integration.Config, "username"), + } + + // Mask API key + apiKey := getString(integration.Config, "api_key") + if len(apiKey) > 4 { + resp.APIKey = strings.Repeat("*", len(apiKey)-4) + apiKey[len(apiKey)-4:] + } else if apiKey != "" { + resp.APIKey = "****" + } + + writeJSON(w, http.StatusOK, resp) +} + +// HandleUpdateOdooConfig updates the Odoo integration configuration. +func (s *Server) HandleUpdateOdooConfig(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var req OdooConfigRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "invalid_json", err.Error()) + return + } + + repo := db.NewIntegrationRepository(s.db) + + // If API key is masked or empty, preserve existing one + if req.APIKey == "" || strings.Contains(req.APIKey, "*") { + existing, err := repo.GetByName(ctx, "odoo") + if err == nil && existing != nil { + req.APIKey = getString(existing.Config, "api_key") + } + } + + config := map[string]any{ + "url": req.URL, + "database": req.Database, + "username": req.Username, + "api_key": req.APIKey, + } + + if err := repo.Upsert(ctx, "odoo", req.Enabled, config); err != nil { + s.logger.Error().Err(err).Msg("failed to update odoo config") + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update Odoo config") + return + } + + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +// HandleGetOdooSyncLog returns recent sync log entries for the Odoo integration. +func (s *Server) HandleGetOdooSyncLog(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + repo := db.NewIntegrationRepository(s.db) + integration, err := repo.GetByName(ctx, "odoo") + if err != nil { + s.logger.Error().Err(err).Msg("failed to get odoo integration") + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to get sync log") + return + } + + if integration == nil { + writeJSON(w, http.StatusOK, []any{}) + return + } + + logs, err := repo.ListSyncLog(ctx, integration.ID, 50) + if err != nil { + s.logger.Error().Err(err).Msg("failed to list sync log") + writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list sync log") + return + } + + if logs == nil { + logs = []*db.SyncLog{} + } + + writeJSON(w, http.StatusOK, logs) +} + +// HandleTestOdooConnection tests the Odoo connection. +func (s *Server) HandleTestOdooConnection(w http.ResponseWriter, r *http.Request) { + // Stub - would use odoo.Client to test connection + writeJSON(w, http.StatusOK, map[string]any{ + "status": "not_implemented", + "message": "Odoo connection testing is not yet implemented", + }) +} + +// HandleOdooPush pushes an item to Odoo. +func (s *Server) HandleOdooPush(w http.ResponseWriter, r *http.Request) { + partNumber := chi.URLParam(r, "partNumber") + if partNumber == "" { + writeError(w, http.StatusBadRequest, "missing_param", "Part number is required") + return + } + + // Stub - log the intent + s.logger.Info().Str("part_number", partNumber).Msg("odoo push requested (stub)") + + writeJSON(w, http.StatusOK, map[string]any{ + "status": "pending", + "message": "Odoo push is not yet implemented", + "part_number": partNumber, + }) +} + +// HandleOdooPull pulls a product from Odoo. +func (s *Server) HandleOdooPull(w http.ResponseWriter, r *http.Request) { + odooID := chi.URLParam(r, "odooId") + if odooID == "" { + writeError(w, http.StatusBadRequest, "missing_param", "Odoo ID is required") + return + } + + // Stub - log the intent + s.logger.Info().Str("odoo_id", odooID).Msg("odoo pull requested (stub)") + + writeJSON(w, http.StatusOK, map[string]any{ + "status": "pending", + "message": "Odoo pull is not yet implemented", + "odoo_id": odooID, + }) +} + +// getString safely extracts a string from a map. +func getString(m map[string]any, key string) string { + if v, ok := m[key]; ok { + if s, ok := v.(string); ok { + return s + } + } + return "" +} diff --git a/internal/api/routes.go b/internal/api/routes.go index 7b482e5..8285f74 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -39,6 +39,7 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler { // Web UI routes r.Get("/", webHandler.HandleIndex) + r.Get("/projects", webHandler.HandleProjectsPage) r.Get("/schemas", webHandler.HandleSchemasPage) // API routes @@ -70,6 +71,7 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler { // Items r.Route("/items", func(r chi.Router) { r.Get("/", server.HandleListItems) + r.Get("/search", server.HandleFuzzySearch) r.Post("/", server.HandleCreateItem) // CSV Import/Export @@ -112,6 +114,16 @@ func NewRouter(server *Server, logger zerolog.Logger) http.Handler { }) }) + // Integrations + r.Route("/integrations/odoo", func(r chi.Router) { + r.Get("/config", server.HandleGetOdooConfig) + r.Put("/config", server.HandleUpdateOdooConfig) + r.Get("/sync-log", server.HandleGetOdooSyncLog) + r.Post("/test-connection", server.HandleTestOdooConnection) + r.Post("/sync/push/{partNumber}", server.HandleOdooPush) + r.Post("/sync/pull/{odooId}", server.HandleOdooPull) + }) + // Part number generation r.Post("/generate-part-number", server.HandleGeneratePartNumber) }) diff --git a/internal/api/search.go b/internal/api/search.go new file mode 100644 index 0000000..eb5cbef --- /dev/null +++ b/internal/api/search.go @@ -0,0 +1,72 @@ +package api + +import ( + "strings" + + "github.com/sahilm/fuzzy" +) + +// SearchableItems implements the fuzzy.Source interface for item search. +type SearchableItems struct { + items []ItemResponse + fields []string +} + +// String returns the searchable text for item at index i. +func (s SearchableItems) String(i int) string { + item := s.items[i] + var parts []string + + for _, field := range s.fields { + switch field { + case "part_number": + parts = append(parts, item.PartNumber) + case "description": + parts = append(parts, item.Description) + case "item_type": + parts = append(parts, item.ItemType) + } + } + + if len(parts) == 0 { + // Default: search all text fields + parts = append(parts, item.PartNumber, item.Description, item.ItemType) + } + + return strings.Join(parts, " ") +} + +// Len returns the number of items. +func (s SearchableItems) Len() int { + return len(s.items) +} + +// FuzzyResult wraps an ItemResponse with match score. +type FuzzyResult struct { + ItemResponse + Score int `json:"score"` +} + +// FuzzySearch runs fuzzy matching on items and returns ranked results. +func FuzzySearch(pattern string, items []ItemResponse, fields []string, limit int) []FuzzyResult { + if pattern == "" || len(items) == 0 { + return nil + } + + source := SearchableItems{items: items, fields: fields} + matches := fuzzy.FindFrom(pattern, source) + + results := make([]FuzzyResult, 0, len(matches)) + for _, m := range matches { + results = append(results, FuzzyResult{ + ItemResponse: items[m.Index], + Score: m.Score, + }) + } + + if limit > 0 && len(results) > limit { + results = results[:limit] + } + + return results +} diff --git a/internal/api/templates/base.html b/internal/api/templates/base.html index d01eee0..c363f05 100644 --- a/internal/api/templates/base.html +++ b/internal/api/templates/base.html @@ -480,6 +480,7 @@ @@ -487,6 +488,8 @@
{{if eq .Page "items"}} {{template "items_content" .}} + {{else if eq .Page "projects"}} + {{template "projects_content" .}} {{else if eq .Page "schemas"}} {{template "schemas_content" .}} {{end}} @@ -494,6 +497,8 @@ {{if eq .Page "items"}} {{template "items_scripts" .}} + {{else if eq .Page "projects"}} + {{template "projects_scripts" .}} {{else if eq .Page "schemas"}} {{template "schemas_scripts" .}} {{end}} diff --git a/internal/api/templates/items.html b/internal/api/templates/items.html index 218a1a3..0b7be81 100644 --- a/internal/api/templates/items.html +++ b/internal/api/templates/items.html @@ -18,35 +18,18 @@ -
+ +

Items

- + +
+ +
+ +
@@ -118,7 +146,7 @@ type="text" class="search-input" id="search-input" - placeholder="Search by part number or description..." + placeholder="Search items... (Ctrl+F)" onkeyup="debounceSearch()" onfocus="showSearchHelp()" onblur="hideSearchHelp()" @@ -139,6 +167,29 @@
+
+ + + +
@@ -151,32 +202,199 @@
+
-
- - - - - - - - - - - - - - - - -
Part NumberTypeDescriptionRevisionCreatedActions
-
-
-
-
+ + + + +
+ +
+
+ + + + + + + + + + + + + + + + +
Part NumberTypeDescriptionRevisionCreatedActions
+
+
+
+
+
+
- + +
+
+ + + + +

Select an item to view details

+

+ Click a row in the item list, or use Ctrl+F to search +

+
+ +
@@ -312,72 +530,6 @@
- - -