package main import ( "fmt" "log" "os" "path/filepath" "verstak/internal/core/actions" "verstak/internal/core/activity" "verstak/internal/core/config" "verstak/internal/core/files" "verstak/internal/core/nodes" "verstak/internal/core/notes" "verstak/internal/core/plugins" "verstak/internal/core/search" "verstak/internal/core/storage" syncsvc "verstak/internal/core/sync" "verstak/internal/core/templates" "verstak/internal/core/vault" "verstak/internal/core/worklog" ) // StartupStatus describes the application startup state. type StartupStatus struct { Status string `json:"status"` // "first_run", "recovery", "ready" VaultPath string `json:"vaultPath"` // configured or default vault path VaultExists bool `json:"vaultExists"` // whether vault.db exists at the path DefaultPath string `json:"defaultPath"` // default vault path suggestion Error string `json:"error,omitempty"` AppConfig *config.AppConfig `json:"appConfig,omitempty"` } // GetStartupStatus checks the global config and vault state. func (a *App) GetStartupStatus() (*StartupStatus, error) { defaultPath, _ := config.DefaultVaultPath() appCfg, err := config.LoadAppConfig() if err != nil { return &StartupStatus{ Status: "first_run", DefaultPath: defaultPath, Error: fmt.Sprintf("config load error: %v", err), }, nil } // No config at all → first run if appCfg == nil { return &StartupStatus{ Status: "first_run", DefaultPath: defaultPath, }, nil } // Config says not completed → first run if !appCfg.FirstRunCompleted { return &StartupStatus{ Status: "first_run", VaultPath: appCfg.VaultPath, DefaultPath: defaultPath, }, nil } // Config has no vault path → first run if appCfg.VaultPath == "" { appCfg.VaultPath = defaultPath _ = config.SaveAppConfig(appCfg) } // Check if vault exists vaultExists := vaultExistsAt(appCfg.VaultPath) if !vaultExists { return &StartupStatus{ Status: "recovery", VaultPath: appCfg.VaultPath, DefaultPath: defaultPath, AppConfig: appCfg, }, nil } // Initialize services so that the vault is ready for use if err := a.initVault(appCfg.VaultPath); err != nil { return &StartupStatus{ Status: "recovery", VaultPath: appCfg.VaultPath, DefaultPath: defaultPath, Error: fmt.Sprintf("init vault: %v", err), }, nil } return &StartupStatus{ Status: "ready", VaultPath: appCfg.VaultPath, VaultExists: true, AppConfig: appCfg, }, nil } func vaultExistsAt(vaultPath string) bool { dbPath := filepath.Join(vaultPath, ".verstak", "index.db") if _, err := os.Stat(dbPath); err != nil { return false } return true } // CreateVault creates a new vault at the given path and initializes all services. func (a *App) CreateVault(vaultPath string) (*StartupStatus, error) { if vaultPath == "" { return nil, fmt.Errorf("vault path is empty") } // Create vault directories and database if err := vault.Init(vaultPath); err != nil { return nil, fmt.Errorf("create vault: %w", err) } // Initialize services for this vault if err := a.initVault(vaultPath); err != nil { return nil, fmt.Errorf("init vault services: %w", err) } // Save global config appCfg, err := config.LoadAppConfig() if err != nil || appCfg == nil { appCfg = config.DefaultAppConfig() } appCfg.VaultPath = vaultPath appCfg.FirstRunCompleted = true if err := config.SaveAppConfig(appCfg); err != nil { return nil, fmt.Errorf("save config: %w", err) } log.Printf("[startup] vault created at %s", vaultPath) return &StartupStatus{ Status: "ready", VaultPath: vaultPath, VaultExists: true, AppConfig: appCfg, }, nil } // OpenVault opens an existing vault and initializes services. func (a *App) OpenVault(vaultPath string) (*StartupStatus, error) { if vaultPath == "" { return nil, fmt.Errorf("vault path is empty") } if !vaultExistsAt(vaultPath) { return nil, fmt.Errorf("vault not found at %s", vaultPath) } if err := a.initVault(vaultPath); err != nil { return nil, fmt.Errorf("init vault: %w", err) } // Update config appCfg, err := config.LoadAppConfig() if err != nil || appCfg == nil { appCfg = config.DefaultAppConfig() } appCfg.VaultPath = vaultPath appCfg.FirstRunCompleted = true if err := config.SaveAppConfig(appCfg); err != nil { return nil, fmt.Errorf("save config: %w", err) } log.Printf("[startup] vault opened at %s", vaultPath) return &StartupStatus{ Status: "ready", VaultPath: vaultPath, VaultExists: true, AppConfig: appCfg, }, nil } // initVault opens the vault DB and initializes all core services. func (a *App) initVault(vaultPath string) error { // Close previous vault if any a.closeVault() abs, err := filepath.Abs(vaultPath) if err != nil { return err } dbPath := filepath.Join(abs, ".verstak", "index.db") db, err := storage.Open(dbPath) if err != nil { return fmt.Errorf("open db: %w", err) } nodeRepo := nodes.NewRepository(db) fileSvc := files.NewService(db, abs, nodeRepo) noteSvc := notes.NewService(db, abs, nodeRepo, fileSvc) actionSvc := actions.NewService(db) activitySvc := activity.NewService(db) worklogSvc := worklog.NewService(db) searchSvc := search.NewService(db) pm := plugins.NewManager(abs) pm.Discover() templatesReg := templates.NewRegistry() if err := templatesReg.LoadSystem(); err != nil { log.Printf("warning: failed to load system templates: %v", err) } // Apply enabled templates from config appCfg, _ := config.LoadAppConfig() if appCfg != nil && len(appCfg.EnabledTemplates) > 0 { enabledSet := make(map[string]bool) for _, id := range appCfg.EnabledTemplates { enabledSet[id] = true } for _, t := range templatesReg.All() { if !enabledSet[t.ID] { _ = templatesReg.Disable(t.ID) } } } // Sync service deviceID := "" _ = appCfg // will store sync settings if appCfg != nil && appCfg.Vault.Sync.DeviceID != "" { deviceID = appCfg.Vault.Sync.DeviceID } if deviceID == "" { deviceID = "gui-" + abs[:8] } syncSvc := syncsvc.NewService(db, deviceID) a.mu.Lock() a.db = db a.nodes = nodeRepo a.files = fileSvc a.notes = noteSvc a.activity = activitySvc a.actions = actionSvc a.worklog = worklogSvc a.search = searchSvc a.plugins = pm a.templates = templatesReg a.sync = syncSvc a.vault = abs a.vaultOpen = true a.mu.Unlock() // Start auto-sync loop go a.autoSyncLoop() return nil } // closeVault shuts down current vault services if any. func (a *App) closeVault() { a.mu.Lock() defer a.mu.Unlock() if !a.vaultOpen { return } if a.db != nil { a.db.Close() } a.db = nil a.nodes = nil a.files = nil a.notes = nil a.activity = nil a.actions = nil a.worklog = nil a.search = nil a.plugins = nil a.templates = nil a.sync = nil a.vault = "" a.vaultOpen = false } // IsReady returns true if a vault is open and services are initialized. func (a *App) IsReady() bool { a.mu.RLock() defer a.mu.RUnlock() return a.vaultOpen } // GetAppConfig returns the current global app config. func (a *App) GetAppConfig() (*config.AppConfig, error) { cfg, err := config.LoadAppConfig() if err != nil { return config.DefaultAppConfig(), nil } if cfg == nil { return config.DefaultAppConfig(), nil } return cfg, nil } // SaveAppConfig saves the global app config. func (a *App) SaveAppConfig(cfg *config.AppConfig) error { return config.SaveAppConfig(cfg) } // GetDefaultVaultPath returns the default vault path. func (a *App) GetDefaultVaultPath() (string, error) { return config.DefaultVaultPath() } // CheckVaultPath checks whether a given path is usable as a vault. type CheckVaultPathResult struct { Exists bool `json:"exists"` HasVault bool `json:"hasVault"` Writable bool `json:"writable"` Description string `json:"description"` } func (a *App) CheckVaultPath(vaultPath string) (*CheckVaultPathResult, error) { if vaultPath == "" { return nil, fmt.Errorf("path is empty") } info, err := os.Stat(vaultPath) exists := err == nil hasVault := false writable := false if exists { if info.IsDir() { writable = checkDirWritable(vaultPath) hasVault = vaultExistsAt(vaultPath) } } else { // Path doesn't exist - check if parent is writable parent := filepath.Dir(vaultPath) parentInfo, pErr := os.Stat(parent) if pErr == nil && parentInfo.IsDir() { writable = checkDirWritable(parent) } } desc := describeVaultPath(vaultPath, exists, hasVault) return &CheckVaultPathResult{ Exists: exists, HasVault: hasVault, Writable: writable, Description: desc, }, nil } func checkDirWritable(dir string) bool { testFile := filepath.Join(dir, ".verstak-write-test") if err := os.WriteFile(testFile, []byte{}, 0o640); err != nil { return false } os.Remove(testFile) return true } func describeVaultPath(path string, exists, hasVault bool) string { if !exists { return "Путь не существует. Будет создан новый vault." } if hasVault { return "Найден существующий vault. Можно подключиться." } return "Папка существует, но vault не найден. Можно создать новый vault." } // VaultInfo returns information about the currently open vault. type VaultInfo struct { Path string `json:"path"` DBPath string `json:"dbPath"` FilesPath string `json:"filesPath"` TrashPath string `json:"trashPath"` Healthy bool `json:"healthy"` NodeCount int `json:"nodeCount"` FileCount int `json:"fileCount"` } func (a *App) GetVaultInfo() (*VaultInfo, error) { if !a.IsReady() { return nil, fmt.Errorf("vault not open") } a.mu.RLock() vp := a.vault nodesCount := 0 if a.nodes != nil { roots, _ := a.nodes.ListRoots(true) nodesCount = len(roots) } fileCount := 0 _ = a.db.QueryRow("SELECT COUNT(*) FROM files").Scan(&fileCount) a.mu.RUnlock() return &VaultInfo{ Path: vp, DBPath: filepath.Join(vp, ".verstak", "index.db"), FilesPath: filepath.Join(vp, "spaces"), TrashPath: filepath.Join(vp, ".verstak", "trash"), Healthy: true, NodeCount: nodesCount, FileCount: fileCount, }, nil }