homelabby/cmd/hwlab/main.go
Mikkel Georgsen f9b1b3ff29 feat(04-04): integrate auto-print into intake handler
- Add IntakePrinter interface to intake.go (optional, nil-safe)
- Add printer field to IntakeHandler, update NewIntakeHandler signature
- Add PrintSkipped bool to IntakeResponse (json: print_skipped)
- Auto-print label after NetBox record creation using labels.RenderStandard + printer.ImageToRawBitmap
- Printer errors are non-fatal: logged and surfaced via print_skipped=true
- Update main.go to pass mockDriver as IntakePrinter
- Add 4 new tests covering success, ErrNoDevice, nil printer, and non-fatal error
- All 10 intake tests pass (6 existing + 4 new)
2026-04-10 06:57:27 +00:00

119 lines
3.7 KiB
Go

package main
import (
"context"
"fmt"
"io/fs"
"log"
"net/http"
"os/signal"
"syscall"
"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/printer"
"git.georgsen.dk/hwlab/internal/queue"
"git.georgsen.dk/hwlab/internal/usb"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}
staticFS, err := fs.Sub(hwlab.StaticFiles, "web/dist")
if err != nil {
log.Fatalf("embed: %v", err)
}
// Context for graceful shutdown
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// NetBox client (required — fatal if misconfigured)
nbClient, err := netbox.NewClient(cfg.NetBoxURL, cfg.NetBoxToken)
if err != nil {
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 waqInstance.RunWorker(ctx, nbHandler, cfg.WAQMaxAttempts, retryInterval)
defer waqInstance.Close()
waqForHandler = waqInstance
log.Printf("WAQ worker started")
}
// USB Manager — polls for device connect/disconnect events.
// Start with 2-second poll interval (production default).
usbManager := usb.NewManager(2 * time.Second)
go usbManager.Start(ctx)
defer usbManager.Stop()
// Printer driver — MockDriver until PRT Qutie hardware arrives (2026-04-13).
// TODO(hardware): replace with printer.NewPrtQutieDriver(9600) after characterization.
mockDriver := printer.NewMockDriver()
if err := mockDriver.Connect(); err != nil {
log.Printf("WARNING: mock printer connect: %v", err)
}
// Intake handler — waqForHandler and mockDriver 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,
mockDriver,
)
inventoryHandler := handlers.NewInventoryHandler(nbClient)
labelHandler := handlers.NewLabelHandler(nbClient, mockDriver)
usbEventsHandler := handlers.NewUSBEventsHandler(usbManager)
router := api.NewRouter(staticFS, intakeHandler, inventoryHandler, labelHandler, usbEventsHandler)
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
log.Printf("HWLab starting on %s", addr)
srv := &http.Server{Addr: addr, Handler: router}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server: %v", err)
}
}()
// Wait for shutdown signal
<-ctx.Done()
log.Println("Shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("server shutdown: %v", err)
}
log.Println("Shutdown complete")
}