fix(qfs): project ui, config naming, sync timestamps - v1.5.4
This commit is contained in:
173
cmd/migrate_project_updated_at/main.go
Normal file
173
cmd/migrate_project_updated_at/main.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.mchus.pro/mchus/quoteforge/internal/appstate"
|
||||
"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"
|
||||
)
|
||||
|
||||
type projectTimestampRow struct {
|
||||
UUID string
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type updatePlanRow struct {
|
||||
UUID string
|
||||
Code string
|
||||
Variant string
|
||||
LocalUpdatedAt time.Time
|
||||
ServerUpdatedAt time.Time
|
||||
}
|
||||
|
||||
func main() {
|
||||
defaultLocalDBPath, err := appstate.ResolveDBPath("")
|
||||
if err != nil {
|
||||
log.Fatalf("failed to resolve default local SQLite path: %v", err)
|
||||
}
|
||||
|
||||
localDBPath := flag.String("localdb", defaultLocalDBPath, "path to local SQLite database (default: user state dir or QFS_DB_PATH)")
|
||||
apply := flag.Bool("apply", false, "apply updates to local SQLite (default is preview only)")
|
||||
flag.Parse()
|
||||
|
||||
local, err := localdb.New(*localDBPath)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize local database: %v", err)
|
||||
}
|
||||
defer local.Close()
|
||||
|
||||
if !local.HasSettings() {
|
||||
log.Fatalf("SQLite connection settings are not configured. Run qfs setup first.")
|
||||
}
|
||||
|
||||
dsn, err := local.GetDSN()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to build DSN from SQLite settings: %v", err)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to connect to MariaDB: %v", err)
|
||||
}
|
||||
|
||||
serverRows, err := loadServerProjects(db)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load server projects: %v", err)
|
||||
}
|
||||
|
||||
localProjects, err := local.GetAllProjects(true)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load local projects: %v", err)
|
||||
}
|
||||
|
||||
plan := buildUpdatePlan(localProjects, serverRows)
|
||||
printPlan(plan, *apply)
|
||||
|
||||
if !*apply || len(plan) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
updated := 0
|
||||
for i := range plan {
|
||||
project, err := local.GetProjectByUUID(plan[i].UUID)
|
||||
if err != nil {
|
||||
log.Printf("skip %s: load local project: %v", plan[i].UUID, err)
|
||||
continue
|
||||
}
|
||||
project.UpdatedAt = plan[i].ServerUpdatedAt
|
||||
if err := local.SaveProjectPreservingUpdatedAt(project); err != nil {
|
||||
log.Printf("skip %s: save local project: %v", plan[i].UUID, err)
|
||||
continue
|
||||
}
|
||||
updated++
|
||||
}
|
||||
|
||||
log.Printf("updated %d local project timestamps", updated)
|
||||
}
|
||||
|
||||
func loadServerProjects(db *gorm.DB) (map[string]time.Time, error) {
|
||||
var rows []projectTimestampRow
|
||||
if err := db.Model(&models.Project{}).
|
||||
Select("uuid, updated_at").
|
||||
Find(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make(map[string]time.Time, len(rows))
|
||||
for _, row := range rows {
|
||||
if row.UUID == "" {
|
||||
continue
|
||||
}
|
||||
out[row.UUID] = row.UpdatedAt
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildUpdatePlan(localProjects []localdb.LocalProject, serverRows map[string]time.Time) []updatePlanRow {
|
||||
plan := make([]updatePlanRow, 0)
|
||||
for i := range localProjects {
|
||||
project := localProjects[i]
|
||||
serverUpdatedAt, ok := serverRows[project.UUID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if project.UpdatedAt.Equal(serverUpdatedAt) {
|
||||
continue
|
||||
}
|
||||
plan = append(plan, updatePlanRow{
|
||||
UUID: project.UUID,
|
||||
Code: project.Code,
|
||||
Variant: project.Variant,
|
||||
LocalUpdatedAt: project.UpdatedAt,
|
||||
ServerUpdatedAt: serverUpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(plan, func(i, j int) bool {
|
||||
if plan[i].Code != plan[j].Code {
|
||||
return plan[i].Code < plan[j].Code
|
||||
}
|
||||
return plan[i].Variant < plan[j].Variant
|
||||
})
|
||||
|
||||
return plan
|
||||
}
|
||||
|
||||
func printPlan(plan []updatePlanRow, apply bool) {
|
||||
mode := "preview"
|
||||
if apply {
|
||||
mode = "apply"
|
||||
}
|
||||
log.Printf("project updated_at resync mode=%s changes=%d", mode, len(plan))
|
||||
if len(plan) == 0 {
|
||||
log.Printf("no local project timestamps need resync")
|
||||
return
|
||||
}
|
||||
for _, row := range plan {
|
||||
variant := row.Variant
|
||||
if variant == "" {
|
||||
variant = "main"
|
||||
}
|
||||
log.Printf("%s [%s] local=%s server=%s", row.Code, variant, formatStamp(row.LocalUpdatedAt), formatStamp(row.ServerUpdatedAt))
|
||||
}
|
||||
if !apply {
|
||||
fmt.Println("Re-run with -apply to write server updated_at into local SQLite.")
|
||||
}
|
||||
}
|
||||
|
||||
func formatStamp(value time.Time) string {
|
||||
if value.IsZero() {
|
||||
return "zero"
|
||||
}
|
||||
return value.Format(time.RFC3339)
|
||||
}
|
||||
Reference in New Issue
Block a user