Add offline RefreshPrices, fix sync bugs, implement auto-restart

- Implement RefreshPrices for local-first mode
  - Update prices from local_components.current_price cache
  - Graceful degradation when component not found
  - Add PriceUpdatedAt timestamp to LocalConfiguration model
  - Support both authenticated and no-auth price refresh

- Fix sync duplicate entry bug
  - pushConfigurationUpdate now ensures server_id exists before update
  - Fetch from LocalConfiguration.ServerID or search on server if missing
  - Update local config with server_id after finding

- Add application auto-restart after settings save
  - Implement restartProcess() using syscall.Exec
  - Setup handler signals restart via channel
  - Setup page polls /health endpoint and redirects when ready
  - Add "Back" button on setup page when settings exist

- Fix setup handler password handling
  - Use PasswordEncrypted field consistently
  - Support empty password by using saved value

- Improve sync status handling
  - Add fallback for is_offline check in SyncStatusPartial
  - Enhance background sync logging with prefixes

- Update CLAUDE.md documentation
  - Mark Phase 2.5 tasks as complete
  - Add UI Improvements section with future tasks
  - Update SQLite tables documentation

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-02-02 11:03:41 +03:00
parent ec3c16f3fc
commit 9bd2acd4f7
11 changed files with 330 additions and 48 deletions

View File

@@ -16,11 +16,12 @@ import (
)
type SetupHandler struct {
localDB *localdb.LocalDB
templates map[string]*template.Template
localDB *localdb.LocalDB
templates map[string]*template.Template
restartSig chan struct{}
}
func NewSetupHandler(localDB *localdb.LocalDB, templatesPath string) (*SetupHandler, error) {
func NewSetupHandler(localDB *localdb.LocalDB, templatesPath string, restartSig chan struct{}) (*SetupHandler, error) {
funcMap := template.FuncMap{
"sub": func(a, b int) int { return a - b },
"add": func(a, b int) int { return a + b },
@@ -37,8 +38,9 @@ func NewSetupHandler(localDB *localdb.LocalDB, templatesPath string) (*SetupHand
templates["setup.html"] = tmpl
return &SetupHandler{
localDB: localDB,
templates: templates,
localDB: localDB,
templates: templates,
restartSig: restartSig,
}, nil
}
@@ -72,6 +74,13 @@ func (h *SetupHandler) TestConnection(c *gin.Context) {
port = p
}
// If password is empty, try to use saved password
if password == "" {
if settings, err := h.localDB.GetSettings(); err == nil && settings != nil {
password = settings.PasswordEncrypted // GetSettings returns decrypted password in this field
}
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s",
user, password, host, port, database)
@@ -138,6 +147,13 @@ func (h *SetupHandler) SaveConnection(c *gin.Context) {
port = p
}
// If password is empty, use saved password
if password == "" {
if settings, err := h.localDB.GetSettings(); err == nil && settings != nil {
password = settings.PasswordEncrypted // GetSettings returns decrypted password in this field
}
}
// Test connection first
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s",
user, password, host, port, database)
@@ -167,8 +183,16 @@ func (h *SetupHandler) SaveConnection(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "Settings saved. Please restart the application.",
"message": "Settings saved. Restarting application...",
})
// Signal restart after response is sent
if h.restartSig != nil {
go func() {
time.Sleep(500 * time.Millisecond) // Give time for response to be sent
h.restartSig <- struct{}{}
}()
}
}
// GetStatus returns the current setup status

View File

@@ -285,20 +285,30 @@ func (h *SyncHandler) GetPendingChanges(c *gin.Context) {
// SyncStatusPartial renders the sync status partial for htmx
// GET /partials/sync-status
func (h *SyncHandler) SyncStatusPartial(c *gin.Context) {
// Check online status
isOffline, _ := c.Get("is_offline")
// Check online status from middleware
isOfflineValue, exists := c.Get("is_offline")
isOffline := false
if exists {
isOffline = isOfflineValue.(bool)
} else {
// Fallback: check directly if middleware didn't set it
isOffline = !h.checkOnline()
slog.Warn("is_offline not found in context, checking directly")
}
// Get pending count
pendingCount := h.localDB.GetPendingCount()
slog.Debug("rendering sync status", "is_offline", isOffline, "pending_count", pendingCount)
data := gin.H{
"IsOffline": isOffline.(bool),
"IsOffline": isOffline,
"PendingCount": pendingCount,
}
c.Header("Content-Type", "text/html; charset=utf-8")
if err := h.tmpl.ExecuteTemplate(c.Writer, "sync_status", data); err != nil {
slog.Error("failed to render sync_status template", "error", err)
c.String(http.StatusInternalServerError, "Template error")
c.String(http.StatusInternalServerError, "Template error: "+err.Error())
}
}