feat: автосинхронизация компонентов для новых пользователей и Support Bundle

- Воркер теперь запускает SyncComponents при пустой local_components,
  чтобы новый пользователь получил каталог компонентов без ручного действия
- Результат синхронизации компонентов персистируется в app_settings
  (last_component_sync_status/error/attempt_at) по аналогии с прайслистами
- Добавлен эндпоинт GET /api/support-bundle: скачивает ZIP с диагностикой
  (app_info, local_db_stats, db_connection с TCP-пингом, sync_readiness,
  system_metrics с памятью и диском, schema_migrations, app.log)
- Кнопка-иконка в шапке рядом с юзернеймом для скачивания бандла

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:50:41 +03:00
parent c951ceb44b
commit 84cab011d3
9 changed files with 294 additions and 0 deletions

View File

@@ -1153,6 +1153,54 @@ func (l *LocalDB) SetPricelistSyncResult(status, errorText string, attemptedAt t
})
}
func (l *LocalDB) GetLastComponentSyncAttemptAt() *time.Time {
value, ok := l.getAppSettingValue("last_component_sync_attempt_at")
if !ok {
return nil
}
t, err := time.Parse(time.RFC3339, value)
if err != nil {
return nil
}
return &t
}
func (l *LocalDB) GetLastComponentSyncStatus() string {
value, ok := l.getAppSettingValue("last_component_sync_status")
if !ok {
return ""
}
return strings.TrimSpace(value)
}
func (l *LocalDB) GetLastComponentSyncError() string {
value, ok := l.getAppSettingValue("last_component_sync_error")
if !ok {
return ""
}
return strings.TrimSpace(value)
}
func (l *LocalDB) SetComponentSyncResult(status, errorText string, attemptedAt time.Time) error {
status = strings.TrimSpace(status)
errorText = strings.TrimSpace(errorText)
if status == "" {
status = "unknown"
}
return l.db.Transaction(func(tx *gorm.DB) error {
if err := l.upsertAppSetting(tx, "last_component_sync_status", status, attemptedAt); err != nil {
return err
}
if err := l.upsertAppSetting(tx, "last_component_sync_error", errorText, attemptedAt); err != nil {
return err
}
if err := l.upsertAppSetting(tx, "last_component_sync_attempt_at", attemptedAt.Format(time.RFC3339), attemptedAt); err != nil {
return err
}
return nil
})
}
// CountLocalPricelists returns the number of local pricelists
func (l *LocalDB) CountLocalPricelists() int64 {
var count int64