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:
parent
4fc9362519
commit
59aa89b199
3 changed files with 54 additions and 7 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue