feat(02-03): wire POST /api/intake route, real WAQ handler, and NetBox defaults in config

- router.go: NewRouter accepts intakeHandler http.Handler, registers POST /api/intake
- config.go: adds NetBoxDefaultDeviceTypeID/RoleID/SiteID fields with defaults and env bindings
- main.go: creates netbox.Client, ai.Orchestrator, inventory.CatalogUpdater, handlers.IntakeHandler
- main.go: replaces NoOpHandler with NewNetBoxOpHandler(nbClient) for WAQ worker
- main.go: uses typed interface variable for WAQ to avoid nil-interface-wrapping bug
This commit is contained in:
Mikkel Georgsen 2026-04-10 05:55:41 +00:00
parent 4fc9362519
commit 59aa89b199
3 changed files with 54 additions and 7 deletions

View file

@ -11,8 +11,12 @@ import (
"time"
hwlab "git.georgsen.dk/hwlab"
"git.georgsen.dk/hwlab/internal/ai"
"git.georgsen.dk/hwlab/internal/api"
"git.georgsen.dk/hwlab/internal/api/handlers"
"git.georgsen.dk/hwlab/internal/config"
"git.georgsen.dk/hwlab/internal/inventory"
"git.georgsen.dk/hwlab/internal/netbox"
"git.georgsen.dk/hwlab/internal/queue"
)
@ -31,18 +35,49 @@ func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// Start write-ahead queue worker (non-fatal if DragonFlyDB unavailable)
waq, err := queue.NewWAQ(cfg.DragonflyURL)
// NetBox client (required — fatal if misconfigured)
nbClient, err := netbox.NewClient(cfg.NetBoxURL, cfg.NetBoxToken)
if err != nil {
log.Printf("WARNING: WAQ unavailable (%v) — NetBox operations will not be queued during downtime", err)
log.Fatalf("netbox client: %v", err)
}
// AI tier clients and orchestrator
tier1 := ai.NewTierClient(cfg.AI.Tier1)
tier2 := ai.NewTierClient(cfg.AI.Tier2)
orch := ai.NewOrchestrator(tier1, tier2, cfg.AI.ConfidenceThreshold)
// Catalog updater
catalogUpdater := inventory.NewCatalogUpdater(nbClient)
// Start write-ahead queue worker (non-fatal if DragonFlyDB unavailable).
// waqForHandler is typed as the interface so that a nil *WAQ is not wrapped in a non-nil interface.
var waqForHandler handlers.IntakeWAQ
waqInstance, waqErr := queue.NewWAQ(cfg.DragonflyURL)
if waqErr != nil {
log.Printf("WARNING: WAQ unavailable (%v) — NetBox operations will not be queued during downtime", waqErr)
} else {
nbHandler := queue.NewNetBoxOpHandler(nbClient)
retryInterval := time.Duration(cfg.WAQRetryIntervalSeconds) * time.Second
go waq.RunWorker(ctx, queue.NoOpHandler, cfg.WAQMaxAttempts, retryInterval)
defer waq.Close()
go waqInstance.RunWorker(ctx, nbHandler, cfg.WAQMaxAttempts, retryInterval)
defer waqInstance.Close()
waqForHandler = waqInstance
log.Printf("WAQ worker started")
}
router := api.NewRouter(staticFS)
// Intake handler — waqForHandler may be nil; handler handles nil gracefully
intakeHandler := handlers.NewIntakeHandler(
orch,
nbClient,
catalogUpdater,
waqForHandler,
cfg.NetBoxDefaultDeviceTypeID,
cfg.NetBoxDefaultRoleID,
cfg.NetBoxDefaultSiteID,
cfg.AI.QuickAddEnabled,
cfg.AI.QuickAddThreshold,
)
router := api.NewRouter(staticFS, intakeHandler)
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
log.Printf("HWLab starting on %s", addr)

View file

@ -32,7 +32,8 @@ func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// NewRouter creates the chi router. staticFiles is the fs.FS rooted at web/dist,
// passed from main.go where the go:embed directive lives.
func NewRouter(staticFiles fs.FS) http.Handler {
// intakeHandler handles POST /api/intake (multipart photo upload).
func NewRouter(staticFiles fs.FS, intakeHandler http.Handler) http.Handler {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
@ -40,6 +41,7 @@ func NewRouter(staticFiles fs.FS) http.Handler {
r.Route("/api", func(r chi.Router) {
r.Get("/health", handlers.Health)
r.Post("/intake", intakeHandler.ServeHTTP)
})
// SPA fallback — serve static files; unknown paths fall back to index.html.

View file

@ -24,6 +24,10 @@ type Config struct {
QualityGateConfidenceThreshold float64 `mapstructure:"quality_gate_confidence_threshold"`
NetBoxDefaultDeviceTypeID int32 `mapstructure:"netbox_default_device_type_id"`
NetBoxDefaultRoleID int32 `mapstructure:"netbox_default_role_id"`
NetBoxDefaultSiteID int32 `mapstructure:"netbox_default_site_id"`
AI ai.AIConfig `mapstructure:"ai"`
}
@ -41,6 +45,9 @@ func Load() (*Config, error) {
v.SetDefault("waq_retry_interval_seconds", 30)
v.SetDefault("waq_max_attempts", 5)
v.SetDefault("quality_gate_confidence_threshold", 0.75)
v.SetDefault("netbox_default_device_type_id", 1)
v.SetDefault("netbox_default_role_id", 1)
v.SetDefault("netbox_default_site_id", 1)
// AI tier defaults
v.SetDefault("ai.tier1.base_url", "http://localhost:8000/v1")
@ -76,6 +83,9 @@ func Load() (*Config, error) {
_ = v.BindEnv("waq_retry_interval_seconds", "HWLAB_WAQ_RETRY_INTERVAL_SECONDS")
_ = v.BindEnv("waq_max_attempts", "HWLAB_WAQ_MAX_ATTEMPTS")
_ = v.BindEnv("quality_gate_confidence_threshold", "HWLAB_QUALITY_GATE_CONFIDENCE_THRESHOLD")
_ = v.BindEnv("netbox_default_device_type_id", "HWLAB_NETBOX_DEFAULT_DEVICE_TYPE_ID")
_ = v.BindEnv("netbox_default_role_id", "HWLAB_NETBOX_DEFAULT_ROLE_ID")
_ = v.BindEnv("netbox_default_site_id", "HWLAB_NETBOX_DEFAULT_SITE_ID")
// AI env bindings
_ = v.BindEnv("ai.tier1.base_url", "HWLAB_AI_TIER1_BASE_URL")