fix: enable instant startup and offline mode for server

Fixed two critical issues preventing offline-first operation:

1. **Instant startup** - Removed blocking GetDB() call during server
   initialization. Server now starts in <10ms instead of 1+ minute.
   - Changed setupRouter() to use lazy DB connection via ConnectionManager
   - mariaDB connection is now nil on startup, established only when needed
   - Fixes timeout issues when MariaDB is unreachable

2. **Offline mode nil pointer panics** - Added graceful degradation
   when database is offline:
   - ComponentService.GetCategories() returns DefaultCategories if repo is nil
   - ComponentService.List/GetByLotName checks for nil repo
   - PricelistService methods return empty/error responses in offline mode
   - All methods properly handle nil repositories

**Before**: Server startup took 1min+ and crashed with nil pointer panic
when trying to load /configurator page offline.

**After**: Server starts instantly and serves pages in offline mode using
DefaultCategories and SQLite data.

Related to Phase 2.5: Full Offline Mode (local-first architecture)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 23:28:14 +03:00
parent 2c75a7ccb8
commit c024b96de7
3 changed files with 141 additions and 60 deletions

View File

@@ -1,6 +1,7 @@
package services
import (
"fmt"
"strings"
"git.mchus.pro/mchus/quoteforge/internal/models"
@@ -59,6 +60,17 @@ type ComponentView struct {
}
func (s *ComponentService) List(filter repository.ComponentFilter, page, perPage int) (*ComponentListResult, error) {
// If no database connection (offline mode), return empty list
// Components should be loaded via /api/sync/components first
if s.componentRepo == nil {
return &ComponentListResult{
Components: []ComponentView{},
Total: 0,
Page: page,
PerPage: perPage,
}, nil
}
if page < 1 {
page = 1
}
@@ -106,6 +118,11 @@ func (s *ComponentService) List(filter repository.ComponentFilter, page, perPage
}
func (s *ComponentService) GetByLotName(lotName string) (*ComponentView, error) {
// If no database connection (offline mode), return error
if s.componentRepo == nil {
return nil, fmt.Errorf("offline mode: component data not available")
}
c, err := s.componentRepo.GetByLotName(lotName)
if err != nil {
return nil, err
@@ -135,11 +152,20 @@ func (s *ComponentService) GetByLotName(lotName string) (*ComponentView, error)
}
func (s *ComponentService) GetCategories() ([]models.Category, error) {
// If no database connection (offline mode), return default categories
if s.categoryRepo == nil {
return models.DefaultCategories, nil
}
return s.categoryRepo.GetAll()
}
// ImportFromLot creates metadata entries for lots that don't have them
func (s *ComponentService) ImportFromLot() (int, error) {
// If no database connection (offline mode), return error
if s.componentRepo == nil || s.categoryRepo == nil {
return 0, fmt.Errorf("offline mode: import not available")
}
lots, err := s.componentRepo.GetLotsWithoutMetadata()
if err != nil {
return 0, err