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>
163 lines
4.5 KiB
Go
163 lines
4.5 KiB
Go
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")
|
|
}
|