diff --git a/cmd/server/main.go b/cmd/server/main.go index 7720997..192991c 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -410,16 +410,22 @@ func setupRouter(cfg *config.Config, local *localdb.LocalDB, connMgr *db.Connect var dbOK bool = false var dbError string - if db, err := connMgr.GetDB(); err == nil && db != nil { - dbOK = true - db.Table("lot").Count(&lotCount) - db.Table("lot_log").Count(&lotLogCount) - db.Table("qt_lot_metadata").Count(&metadataCount) + // Check if connection exists (fast check, no reconnect attempt) + status := connMgr.GetStatus() + if status.IsConnected { + // Already connected, safe to use + if db, err := connMgr.GetDB(); err == nil && db != nil { + dbOK = true + db.Table("lot").Count(&lotCount) + db.Table("lot_log").Count(&lotLogCount) + db.Table("qt_lot_metadata").Count(&metadataCount) + } } else { - if err != nil { - dbError = err.Error() - } else { - dbError = "Database not connected (offline mode)" + // Not connected - don't try to reconnect on status check + // This prevents 3s timeout on every request + dbError = "Database not connected (offline mode)" + if status.LastError != "" { + dbError = status.LastError } } diff --git a/internal/handlers/pricelist.go b/internal/handlers/pricelist.go index ca91936..19c2b40 100644 --- a/internal/handlers/pricelist.go +++ b/internal/handlers/pricelist.go @@ -124,9 +124,33 @@ func (h *PricelistHandler) CanWrite(c *gin.Context) { // GetLatest returns the most recent active pricelist func (h *PricelistHandler) GetLatest(c *gin.Context) { + // Try to get from server first pl, err := h.service.GetLatestActive() if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "no active pricelists found"}) + // If offline or no server pricelists, try to get from local cache + if h.localDB == nil { + c.JSON(http.StatusNotFound, gin.H{"error": "no database available"}) + return + } + localPL, localErr := h.localDB.GetLatestLocalPricelist() + if localErr != nil { + // No local pricelists either + c.JSON(http.StatusNotFound, gin.H{ + "error": "no pricelists available", + "local_error": localErr.Error(), + }) + return + } + // Return local pricelist + c.JSON(http.StatusOK, gin.H{ + "id": localPL.ServerID, + "version": localPL.Version, + "created_by": "sync", + "item_count": 0, // Not tracked in local pricelists + "is_active": true, + "created_at": localPL.CreatedAt, + "synced_from": "local", + }) return } diff --git a/internal/localdb/localdb.go b/internal/localdb/localdb.go index 9558e38..1591e51 100644 --- a/internal/localdb/localdb.go +++ b/internal/localdb/localdb.go @@ -132,7 +132,11 @@ func (l *LocalDB) GetDSN() (string, error) { return "", err } - dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + // Add aggressive timeouts for offline-first architecture + // timeout: connection establishment timeout (3s) + // readTimeout: I/O read timeout (3s) + // writeTimeout: I/O write timeout (3s) + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=3s&readTimeout=3s&writeTimeout=3s", settings.User, settings.PasswordEncrypted, // Contains decrypted password after GetSettings settings.Host, diff --git a/internal/services/sync/service.go b/internal/services/sync/service.go index b1f431f..9c0bba7 100644 --- a/internal/services/sync/service.go +++ b/internal/services/sync/service.go @@ -38,13 +38,16 @@ type SyncStatus struct { func (s *Service) GetStatus() (*SyncStatus, error) { lastSync := s.localDB.GetLastSyncTime() - // Count server pricelists (requires connection) + // Count server pricelists (only if already connected, don't reconnect) serverCount := 0 - if mariaDB, err := s.connMgr.GetDB(); err == nil && mariaDB != nil { - pricelistRepo := repository.NewPricelistRepository(mariaDB) - serverPricelists, _, err := pricelistRepo.List(0, 1) - if err == nil { - serverCount = len(serverPricelists) + connStatus := s.connMgr.GetStatus() + if connStatus.IsConnected { + if mariaDB, err := s.connMgr.GetDB(); err == nil && mariaDB != nil { + pricelistRepo := repository.NewPricelistRepository(mariaDB) + serverPricelists, _, err := pricelistRepo.List(0, 1) + if err == nil { + serverCount = len(serverPricelists) + } } } @@ -76,7 +79,13 @@ func (s *Service) NeedSync() (bool, error) { return true, nil } - // Check if there are new pricelists on server (requires connection) + // Check if there are new pricelists on server (only if already connected) + connStatus := s.connMgr.GetStatus() + if !connStatus.IsConnected { + // If offline, can't check server, no need to sync + return false, nil + } + mariaDB, err := s.connMgr.GetDB() if err != nil { // If offline, can't check server, no need to sync diff --git a/web/templates/configs.html b/web/templates/configs.html index c962bdb..a91b754 100644 --- a/web/templates/configs.html +++ b/web/templates/configs.html @@ -421,18 +421,23 @@ async function loadLatestPricelistVersion() { const pricelist = await resp.json(); document.getElementById('pricelist-version').textContent = pricelist.version; document.getElementById('pricelist-badge').classList.remove('hidden'); + } else if (resp.status === 404) { + // No active pricelist (normal in offline mode or when not synced) + document.getElementById('pricelist-version').textContent = 'Не загружен'; + document.getElementById('pricelist-badge').classList.remove('hidden'); + document.getElementById('pricelist-badge').classList.add('bg-gray-100', 'text-gray-600'); } else { - // Show error in badge + // Real error document.getElementById('pricelist-version').textContent = 'Ошибка загрузки'; document.getElementById('pricelist-badge').classList.remove('hidden'); document.getElementById('pricelist-badge').classList.add('bg-red-100', 'text-red-800'); } } catch(e) { - // Show error in badge + // Network error or other exception console.error('Failed to load pricelist version:', e); - document.getElementById('pricelist-version').textContent = 'Ошибка загрузки'; + document.getElementById('pricelist-version').textContent = 'Не доступен'; document.getElementById('pricelist-badge').classList.remove('hidden'); - document.getElementById('pricelist-badge').classList.add('bg-red-100', 'text-red-800'); + document.getElementById('pricelist-badge').classList.add('bg-gray-100', 'text-gray-600'); } }