Add Phase 2: Local SQLite database with sync functionality
Implements complete offline-first architecture with SQLite caching and MariaDB synchronization. Key features: - Local SQLite database for offline operation (data/quoteforge.db) - Connection settings with encrypted credentials - Component and pricelist caching with auto-sync - Sync API endpoints (/api/sync/status, /components, /pricelists, /all) - Real-time sync status indicator in UI with auto-refresh - Offline mode detection middleware - Migration tool for database initialization - Setup wizard for initial configuration New components: - internal/localdb: SQLite repository layer (components, pricelists, sync) - internal/services/sync: Synchronization service - internal/handlers/sync: Sync API handlers - internal/handlers/setup: Setup wizard handlers - internal/middleware/offline: Offline detection - cmd/migrate: Database migration tool UI improvements: - Setup page for database configuration - Sync status indicator with online/offline detection - Warning icons for pending synchronization - Auto-refresh every 30 seconds Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
162
cmd/migrate/main.go
Normal file
162
cmd/migrate/main.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/config"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
||||
"git.mchus.pro/mchus/quoteforge/internal/models"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.yaml", "path to config file")
|
||||
localDBPath := flag.String("localdb", "./data/settings.db", "path to local SQLite database")
|
||||
dryRun := flag.Bool("dry-run", false, "show what would be migrated without actually doing it")
|
||||
flag.Parse()
|
||||
|
||||
log.Println("QuoteForge Configuration Migration Tool")
|
||||
log.Println("========================================")
|
||||
|
||||
// Load config for MariaDB connection
|
||||
cfg, err := config.Load(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Connect to MariaDB
|
||||
log.Printf("Connecting to MariaDB at %s:%d...", cfg.Database.Host, cfg.Database.Port)
|
||||
mariaDB, err := gorm.Open(mysql.Open(cfg.Database.DSN()), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to MariaDB: %v", err)
|
||||
}
|
||||
log.Println("Connected to MariaDB")
|
||||
|
||||
// Initialize local SQLite
|
||||
log.Printf("Opening local SQLite at %s...", *localDBPath)
|
||||
local, err := localdb.New(*localDBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize local database: %v", err)
|
||||
}
|
||||
log.Println("Local SQLite initialized")
|
||||
|
||||
// Count configurations in MariaDB
|
||||
var serverCount int64
|
||||
if err := mariaDB.Model(&models.Configuration{}).Count(&serverCount).Error; err != nil {
|
||||
log.Fatalf("Failed to count configurations: %v", err)
|
||||
}
|
||||
log.Printf("Found %d configurations in MariaDB", serverCount)
|
||||
|
||||
if serverCount == 0 {
|
||||
log.Println("No configurations to migrate")
|
||||
return
|
||||
}
|
||||
|
||||
// Get all configurations from MariaDB
|
||||
var configs []models.Configuration
|
||||
if err := mariaDB.Preload("User").Find(&configs).Error; err != nil {
|
||||
log.Fatalf("Failed to fetch configurations: %v", err)
|
||||
}
|
||||
|
||||
// Check existing local configurations
|
||||
localCount := local.CountConfigurations()
|
||||
log.Printf("Found %d configurations in local SQLite", localCount)
|
||||
|
||||
if *dryRun {
|
||||
log.Println("\n[DRY RUN] Would migrate the following configurations:")
|
||||
for _, c := range configs {
|
||||
userName := "unknown"
|
||||
if c.User != nil {
|
||||
userName = c.User.Username
|
||||
}
|
||||
log.Printf(" - %s (UUID: %s, User: %s, Items: %d)", c.Name, c.UUID, userName, len(c.Items))
|
||||
}
|
||||
log.Printf("\nTotal: %d configurations", len(configs))
|
||||
return
|
||||
}
|
||||
|
||||
// Migrate configurations
|
||||
log.Println("\nMigrating configurations...")
|
||||
migrated := 0
|
||||
skipped := 0
|
||||
errors := 0
|
||||
|
||||
for _, c := range configs {
|
||||
// Check if already exists
|
||||
existing, err := local.GetConfigurationByUUID(c.UUID)
|
||||
if err == nil && existing.ID > 0 {
|
||||
log.Printf(" SKIP: %s (already exists)", c.Name)
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
// Convert items
|
||||
localItems := make(localdb.LocalConfigItems, len(c.Items))
|
||||
for i, item := range c.Items {
|
||||
localItems[i] = localdb.LocalConfigItem{
|
||||
LotName: item.LotName,
|
||||
Quantity: item.Quantity,
|
||||
UnitPrice: item.UnitPrice,
|
||||
}
|
||||
}
|
||||
|
||||
// Create local configuration
|
||||
now := time.Now()
|
||||
localConfig := &localdb.LocalConfiguration{
|
||||
UUID: c.UUID,
|
||||
ServerID: &c.ID,
|
||||
Name: c.Name,
|
||||
Items: localItems,
|
||||
TotalPrice: c.TotalPrice,
|
||||
CustomPrice: c.CustomPrice,
|
||||
Notes: c.Notes,
|
||||
IsTemplate: c.IsTemplate,
|
||||
ServerCount: c.ServerCount,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: now,
|
||||
SyncedAt: &now,
|
||||
SyncStatus: "synced",
|
||||
OriginalUserID: c.UserID,
|
||||
}
|
||||
|
||||
if err := local.SaveConfiguration(localConfig); err != nil {
|
||||
log.Printf(" ERROR: %s - %v", c.Name, err)
|
||||
errors++
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf(" OK: %s (%d items)", c.Name, len(c.Items))
|
||||
migrated++
|
||||
}
|
||||
|
||||
log.Println("\n========================================")
|
||||
log.Printf("Migration complete!")
|
||||
log.Printf(" Migrated: %d", migrated)
|
||||
log.Printf(" Skipped: %d", skipped)
|
||||
log.Printf(" Errors: %d", errors)
|
||||
|
||||
// Save connection settings to local SQLite if not exists
|
||||
if !local.HasSettings() {
|
||||
log.Println("\nSaving connection settings to local SQLite...")
|
||||
if err := local.SaveSettings(
|
||||
cfg.Database.Host,
|
||||
cfg.Database.Port,
|
||||
cfg.Database.Name,
|
||||
cfg.Database.User,
|
||||
cfg.Database.Password,
|
||||
); err != nil {
|
||||
log.Printf("Warning: Failed to save settings: %v", err)
|
||||
} else {
|
||||
log.Println("Connection settings saved")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("\nDone! You can now run the server with: go run ./cmd/server")
|
||||
}
|
||||
Reference in New Issue
Block a user