fix: vault init on startup; add nil guards to all bindings; fix SA_ONSTACK signal crash; deduplicate settings button; add i18n for vault error

This commit is contained in:
mirivlad 2026-06-04 00:37:14 +08:00
parent f92394e3d7
commit a69dc845e6
26 changed files with 259 additions and 21 deletions

View File

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"path/filepath" "path/filepath"
"sync" "sync"
@ -44,6 +45,15 @@ type App struct {
vault string vault string
} }
// requireVault returns an error if no vault is open and services are not initialized.
// All binding methods that access vault services MUST call this first.
func (a *App) requireVault() error {
if !a.IsReady() {
return fmt.Errorf("vault not open")
}
return nil
}
// startup is called when the app starts. Store context and wire drag-and-drop. // startup is called when the app starts. Store context and wire drag-and-drop.
func (a *App) startup(ctx context.Context) { func (a *App) startup(ctx context.Context) {
a.ctx = ctx a.ctx = ctx

View File

@ -5,6 +5,9 @@ import (
) )
func (a *App) ListActions(nodeID string) ([]ActionDTO, error) { func (a *App) ListActions(nodeID string) ([]ActionDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
list, err := a.actions.ListByNode(nodeID) list, err := a.actions.ListByNode(nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -27,6 +30,9 @@ func (a *App) ListActions(nodeID string) ([]ActionDTO, error) {
} }
func (a *App) CreateAction(nodeID, kind, title, data string) (*ActionDTO, error) { func (a *App) CreateAction(nodeID, kind, title, data string) (*ActionDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
rec, err := a.actions.Create(nodeID, kind, title, data, "", data, nil, kind == "run_command" || kind == "run_script", false) rec, err := a.actions.Create(nodeID, kind, title, data, "", data, nil, kind == "run_command" || kind == "run_script", false)
if err != nil { if err != nil {
return nil, err return nil, err
@ -42,11 +48,17 @@ func (a *App) CreateAction(nodeID, kind, title, data string) (*ActionDTO, error)
} }
func (a *App) DeleteAction(id string) error { func (a *App) DeleteAction(id string) error {
if err := a.requireVault(); err != nil {
return err
}
_ = a.sync.RecordOp(syncsvc.EntityAction, id, syncsvc.OpDelete, nil) _ = a.sync.RecordOp(syncsvc.EntityAction, id, syncsvc.OpDelete, nil)
return a.actions.Delete(id) return a.actions.Delete(id)
} }
func (a *App) RunAction(id string) error { func (a *App) RunAction(id string) error {
if err := a.requireVault(); err != nil {
return err
}
_, err := a.actions.Run(id) _, err := a.actions.Run(id)
return err return err
} }

View File

@ -25,6 +25,9 @@ func (a *App) ListSystemViews() []SystemViewDTO {
} }
func (a *App) ListTodayView() (*TodayDashboardDTO, error) { func (a *App) ListTodayView() (*TodayDashboardDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
aeByParent, err := a.activity.ListTodayEventsByParent() aeByParent, err := a.activity.ListTodayEventsByParent()
if err != nil { if err != nil {
aeByParent = nil aeByParent = nil
@ -144,6 +147,9 @@ func (a *App) ListTodayView() (*TodayDashboardDTO, error) {
} }
func (a *App) ListActivityFeed(limit, offset int) ([]EventDTO, error) { func (a *App) ListActivityFeed(limit, offset int) ([]EventDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
events, err := a.activity.ListRecent(limit, offset) events, err := a.activity.ListRecent(limit, offset)
if err != nil { if err != nil {
return nil, err return nil, err
@ -156,6 +162,9 @@ func (a *App) ListActivityFeed(limit, offset int) ([]EventDTO, error) {
} }
func (a *App) ListActivityByNode(nodeID string, limit, offset int) ([]EventDTO, error) { func (a *App) ListActivityByNode(nodeID string, limit, offset int) ([]EventDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
events, err := a.activity.ListByNode(nodeID, limit, offset) events, err := a.activity.ListByNode(nodeID, limit, offset)
if err != nil { if err != nil {
return nil, err return nil, err
@ -168,6 +177,9 @@ func (a *App) ListActivityByNode(nodeID string, limit, offset int) ([]EventDTO,
} }
func (a *App) CountActivityByNode(nodeID string) (int, error) { func (a *App) CountActivityByNode(nodeID string) (int, error) {
if err := a.requireVault(); err != nil {
return 0, err
}
return a.activity.CountByNode(nodeID) return a.activity.CountByNode(nodeID)
} }

View File

@ -78,6 +78,16 @@ func (a *App) GetStartupStatus() (*StartupStatus, error) {
}, nil }, 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{ return &StartupStatus{
Status: "ready", Status: "ready",
VaultPath: appCfg.VaultPath, VaultPath: appCfg.VaultPath,

View File

@ -10,6 +10,9 @@ import (
// WriteDebugLog appends a line to <vault>/.verstak/debug.log. // WriteDebugLog appends a line to <vault>/.verstak/debug.log.
// Called from frontend to log JS-side diagnostics in production GUI builds. // Called from frontend to log JS-side diagnostics in production GUI builds.
func (a *App) WriteDebugLog(msg string) { func (a *App) WriteDebugLog(msg string) {
if !a.IsReady() {
return
}
logPath := filepath.Join(a.vault, ".verstak", "debug.log") logPath := filepath.Join(a.vault, ".verstak", "debug.log")
line := fmt.Sprintf("[%s] %s\n", time.Now().Format("2006-01-02T15:04:05"), msg) line := fmt.Sprintf("[%s] %s\n", time.Now().Format("2006-01-02T15:04:05"), msg)
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)

View File

@ -8,6 +8,9 @@ import (
) )
func (a *App) ListFiles(nodeID string) ([]FileDTO, error) { func (a *App) ListFiles(nodeID string) ([]FileDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
records, err := a.files.ListByNode(nodeID) records, err := a.files.ListByNode(nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -30,6 +33,9 @@ func (a *App) ListFiles(nodeID string) ([]FileDTO, error) {
} }
func (a *App) ListItems(nodeID string) ([]FileTreeItemDTO, error) { func (a *App) ListItems(nodeID string) ([]FileTreeItemDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
children, err := a.nodes.ListChildren(nodeID, false) children, err := a.nodes.ListChildren(nodeID, false)
if err != nil { if err != nil {
return nil, err return nil, err
@ -61,6 +67,9 @@ func (a *App) ListItems(nodeID string) ([]FileTreeItemDTO, error) {
} }
func (a *App) AddPathCopy(nodeID, sourcePath string) ([]NodeDTO, error) { func (a *App) AddPathCopy(nodeID, sourcePath string) ([]NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
nodes, err := a.files.AddPathCopy(nodeID, sourcePath) nodes, err := a.files.AddPathCopy(nodeID, sourcePath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -73,6 +82,9 @@ func (a *App) AddPathCopy(nodeID, sourcePath string) ([]NodeDTO, error) {
} }
func (a *App) AddPathLink(nodeID, sourcePath string) ([]NodeDTO, error) { func (a *App) AddPathLink(nodeID, sourcePath string) ([]NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
nodes, err := a.files.AddPathLink(nodeID, sourcePath) nodes, err := a.files.AddPathLink(nodeID, sourcePath)
if err != nil { if err != nil {
return nil, err return nil, err
@ -85,6 +97,9 @@ func (a *App) AddPathLink(nodeID, sourcePath string) ([]NodeDTO, error) {
} }
func (a *App) DeleteFileOrFolder(nodeID string) error { func (a *App) DeleteFileOrFolder(nodeID string) error {
if err := a.requireVault(); err != nil {
return err
}
n, err := a.nodes.GetActive(nodeID) n, err := a.nodes.GetActive(nodeID)
if err == nil { if err == nil {
pid := "" pid := ""
@ -108,6 +123,9 @@ func (a *App) DeleteFileOrFolder(nodeID string) error {
} }
func (a *App) CreateEmptyFile(parentID, filename string) (*NodeDTO, error) { func (a *App) CreateEmptyFile(parentID, filename string) (*NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
node, err := a.files.CreateEmptyFile(parentID, filename) node, err := a.files.CreateEmptyFile(parentID, filename)
if err != nil { if err != nil {
return nil, err return nil, err
@ -119,6 +137,9 @@ func (a *App) CreateEmptyFile(parentID, filename string) (*NodeDTO, error) {
} }
func (a *App) DuplicateNode(nodeID string) (*NodeDTO, error) { func (a *App) DuplicateNode(nodeID string) (*NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
node, err := a.files.Duplicate(nodeID) node, err := a.files.Duplicate(nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -139,5 +160,8 @@ func (a *App) ValidateName(name string) error {
} }
func (a *App) PreviewImport(sourcePath string) (*files.ImportSummary, error) { func (a *App) PreviewImport(sourcePath string) (*files.ImportSummary, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
return a.files.PreviewImport(sourcePath) return a.files.PreviewImport(sourcePath)
} }

View File

@ -15,6 +15,9 @@ import (
) )
func (a *App) ListWorkspaceTree() ([]NodeDTO, error) { func (a *App) ListWorkspaceTree() ([]NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
list, err := a.nodes.ListRoots(false) list, err := a.nodes.ListRoots(false)
if err != nil { if err != nil {
return nil, err return nil, err
@ -31,6 +34,9 @@ func (a *App) ListWorkspaceTree() ([]NodeDTO, error) {
} }
func (a *App) ListWorkspaceChildren(parentID string) ([]NodeDTO, error) { func (a *App) ListWorkspaceChildren(parentID string) ([]NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
list, err := a.nodes.ListChildren(parentID, false) list, err := a.nodes.ListChildren(parentID, false)
if err != nil { if err != nil {
return nil, err return nil, err
@ -65,6 +71,9 @@ func isContainerType(typ string) bool {
} }
func (a *App) ListChildren(parentID string) ([]NodeDTO, error) { func (a *App) ListChildren(parentID string) ([]NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
list, err := a.nodes.ListChildren(parentID, false) list, err := a.nodes.ListChildren(parentID, false)
if err != nil { if err != nil {
return nil, err return nil, err
@ -73,6 +82,9 @@ func (a *App) ListChildren(parentID string) ([]NodeDTO, error) {
} }
func (a *App) GetNodeDetail(nodeID string) (*NodeDTO, error) { func (a *App) GetNodeDetail(nodeID string) (*NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
n, err := a.nodes.GetActive(nodeID) n, err := a.nodes.GetActive(nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -82,6 +94,9 @@ func (a *App) GetNodeDetail(nodeID string) (*NodeDTO, error) {
} }
func (a *App) GetNodeTitle(nodeID string) (string, error) { func (a *App) GetNodeTitle(nodeID string) (string, error) {
if err := a.requireVault(); err != nil {
return "", err
}
n, err := a.nodes.GetActive(nodeID) n, err := a.nodes.GetActive(nodeID)
if err != nil { if err != nil {
return "", err return "", err
@ -90,6 +105,9 @@ func (a *App) GetNodeTitle(nodeID string) (string, error) {
} }
func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeDTO, error) { func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
tmpl, ok := a.templates.Get(templateID) tmpl, ok := a.templates.Get(templateID)
if !ok { if !ok {
return nil, fmt.Errorf("template %q not found", templateID) return nil, fmt.Errorf("template %q not found", templateID)
@ -246,6 +264,9 @@ func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeD
} }
func (a *App) DeleteNode(id string) error { func (a *App) DeleteNode(id string) error {
if err := a.requireVault(); err != nil {
return err
}
n, err := a.nodes.GetActive(id) n, err := a.nodes.GetActive(id)
if err != nil { if err != nil {
return a.nodes.SoftDelete(id) return a.nodes.SoftDelete(id)
@ -282,6 +303,9 @@ func (a *App) DeleteNode(id string) error {
} }
func (a *App) RenameNode(nodeID, newTitle string) error { func (a *App) RenameNode(nodeID, newTitle string) error {
if err := a.requireVault(); err != nil {
return err
}
n, err := a.nodes.GetActive(nodeID) n, err := a.nodes.GetActive(nodeID)
if err != nil { if err != nil {
return err return err
@ -574,6 +598,9 @@ func (a *App) wouldCreateCycle(nodeID, newParentID string) error {
} }
func (a *App) MoveNode(nodeID, newParentID string) error { func (a *App) MoveNode(nodeID, newParentID string) error {
if err := a.requireVault(); err != nil {
return err
}
if nodeID == "" { if nodeID == "" {
return fmt.Errorf("node ID is required") return fmt.Errorf("node ID is required")
} }
@ -918,6 +945,9 @@ type SearchNodeResult struct {
} }
func (a *App) SearchNodes(query string) ([]SearchNodeResult, error) { func (a *App) SearchNodes(query string) ([]SearchNodeResult, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
nodes, err := a.nodes.Search(query, 20) nodes, err := a.nodes.Search(query, 20)
if err != nil { if err != nil {
return nil, err return nil, err
@ -939,6 +969,9 @@ func (a *App) SearchNodes(query string) ([]SearchNodeResult, error) {
} }
func (a *App) OpenNodeFolder(nodeID string) (string, error) { func (a *App) OpenNodeFolder(nodeID string) (string, error) {
if err := a.requireVault(); err != nil {
return "", err
}
n, err := a.nodes.GetActive(nodeID) n, err := a.nodes.GetActive(nodeID)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -9,6 +9,9 @@ import (
) )
func (a *App) ListNotes(nodeID string) ([]NodeDTO, error) { func (a *App) ListNotes(nodeID string) ([]NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
children, err := a.nodes.ListChildren(nodeID, false) children, err := a.nodes.ListChildren(nodeID, false)
if err != nil { if err != nil {
return nil, err return nil, err
@ -23,6 +26,9 @@ func (a *App) ListNotes(nodeID string) ([]NodeDTO, error) {
} }
func (a *App) CreateNote(parentID, title string) (*NodeDTO, error) { func (a *App) CreateNote(parentID, title string) (*NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
node, fileRec, err := a.notes.Create(parentID, title, "") node, fileRec, err := a.notes.Create(parentID, title, "")
if err != nil { if err != nil {
return nil, err return nil, err
@ -34,10 +40,16 @@ func (a *App) CreateNote(parentID, title string) (*NodeDTO, error) {
} }
func (a *App) ReadNote(noteID string) (string, error) { func (a *App) ReadNote(noteID string) (string, error) {
if err := a.requireVault(); err != nil {
return "", err
}
return a.notes.Read(noteID) return a.notes.Read(noteID)
} }
func (a *App) SaveNote(noteID, content string) error { func (a *App) SaveNote(noteID, content string) error {
if err := a.requireVault(); err != nil {
return err
}
if err := a.notes.Save(noteID, content); err != nil { if err := a.notes.Save(noteID, content); err != nil {
return err return err
} }

View File

@ -95,6 +95,9 @@ func (a *App) SetTemplateEnabled(templateID string, enabled bool) error {
} }
func (a *App) ListTemplates() []TemplateDTO { func (a *App) ListTemplates() []TemplateDTO {
if !a.IsReady() {
return nil
}
templates := a.plugins.Templates() templates := a.plugins.Templates()
out := make([]TemplateDTO, 0, len(templates)) out := make([]TemplateDTO, 0, len(templates))
for _, t := range templates { for _, t := range templates {
@ -109,6 +112,9 @@ func (a *App) ListTemplates() []TemplateDTO {
} }
func (a *App) FromTemplate(parentID, nodeType, title, section, template string) (*NodeDTO, error) { func (a *App) FromTemplate(parentID, nodeType, title, section, template string) (*NodeDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
var tmpl *plugins.TemplateDefinition var tmpl *plugins.TemplateDefinition
for _, t := range a.plugins.Templates() { for _, t := range a.plugins.Templates() {
if t.Name == template { if t.Name == template {
@ -166,18 +172,30 @@ func (a *App) PickDirectory() (string, error) {
} }
func (a *App) OpenFile(fileID string) error { func (a *App) OpenFile(fileID string) error {
if err := a.requireVault(); err != nil {
return err
}
return a.files.Open(fileID) return a.files.Open(fileID)
} }
func (a *App) ReadFileText(fileID string) (string, error) { func (a *App) ReadFileText(fileID string) (string, error) {
if err := a.requireVault(); err != nil {
return "", err
}
return a.files.ReadText(fileID) return a.files.ReadText(fileID)
} }
func (a *App) GetFileBase64(fileID string) (string, error) { func (a *App) GetFileBase64(fileID string) (string, error) {
if err := a.requireVault(); err != nil {
return "", err
}
return a.files.ReadBase64(fileID) return a.files.ReadBase64(fileID)
} }
func (a *App) OpenFolder(nodeID string) error { func (a *App) OpenFolder(nodeID string) error {
if err := a.requireVault(); err != nil {
return err
}
n, err := a.nodes.GetActive(nodeID) n, err := a.nodes.GetActive(nodeID)
if err != nil { if err != nil {
return err return err
@ -214,6 +232,9 @@ func (a *App) OpenVaultFolder() error {
} }
func (a *App) Search(query string) ([]SearchResultDTO, error) { func (a *App) Search(query string) ([]SearchResultDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
if query == "" { if query == "" {
return []SearchResultDTO{}, nil return []SearchResultDTO{}, nil
} }

View File

@ -15,6 +15,9 @@ import (
// GetSuggestions analyzes today's activity and returns conservative suggestions. // GetSuggestions analyzes today's activity and returns conservative suggestions.
// Only events not already linked in worklog_entry_events are considered. // Only events not already linked in worklog_entry_events are considered.
func (a *App) GetSuggestions() ([]activity.Suggestion, error) { func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
events, err := a.activity.ListTodayEvents() events, err := a.activity.ListTodayEvents()
if err != nil || len(events) == 0 { if err != nil || len(events) == 0 {
return nil, err return nil, err
@ -114,12 +117,18 @@ func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
// AcceptSuggestion creates a worklog entry from a suggestion (compatibility wrapper). // AcceptSuggestion creates a worklog entry from a suggestion (compatibility wrapper).
func (a *App) AcceptSuggestion(nodeID, summary string, minutes int, date string, eventIDsJSON string) (*WorklogDTO, error) { func (a *App) AcceptSuggestion(nodeID, summary string, minutes int, date string, eventIDsJSON string) (*WorklogDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
return a.AcceptSuggestionWith(nodeID, summary, minutes, date, eventIDsJSON) return a.AcceptSuggestionWith(nodeID, summary, minutes, date, eventIDsJSON)
} }
// AcceptSuggestionWith creates a worklog entry and links events in a single transaction. // AcceptSuggestionWith creates a worklog entry and links events in a single transaction.
// eventIDsJSON is a JSON-serialized string array to avoid Wails v2 []string marshalling issues. // eventIDsJSON is a JSON-serialized string array to avoid Wails v2 []string marshalling issues.
func (a *App) AcceptSuggestionWith(nodeID, summary string, minutes int, date string, eventIDsJSON string) (*WorklogDTO, error) { func (a *App) AcceptSuggestionWith(nodeID, summary string, minutes int, date string, eventIDsJSON string) (*WorklogDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
d := date d := date
if d == "" { if d == "" {
d = time.Now().Format("2006-01-02") d = time.Now().Format("2006-01-02")

View File

@ -114,6 +114,9 @@ type SyncSettingsDTO struct {
} }
func (a *App) GetSyncSettings() (*SyncSettingsDTO, error) { func (a *App) GetSyncSettings() (*SyncSettingsDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
appCfg, _ := config.LoadAppConfig() appCfg, _ := config.LoadAppConfig()
if appCfg == nil { if appCfg == nil {
appCfg = config.DefaultAppConfig() appCfg = config.DefaultAppConfig()
@ -133,6 +136,9 @@ func (a *App) GetSyncSettings() (*SyncSettingsDTO, error) {
} }
func (a *App) SyncConfigure(serverURL, username, password string) error { func (a *App) SyncConfigure(serverURL, username, password string) error {
if err := a.requireVault(); err != nil {
return err
}
hostname, _ := os.Hostname() hostname, _ := os.Hostname()
if hostname == "" { if hostname == "" {
hostname = "unknown" hostname = "unknown"
@ -165,6 +171,9 @@ func (a *App) SyncConfigure(serverURL, username, password string) error {
} }
func (a *App) SyncDisconnect() error { func (a *App) SyncDisconnect() error {
if err := a.requireVault(); err != nil {
return err
}
deviceToken := config.LoadDeviceToken(a.vault) deviceToken := config.LoadDeviceToken(a.vault)
appCfg, _ := config.LoadAppConfig() appCfg, _ := config.LoadAppConfig()
if appCfg == nil { if appCfg == nil {
@ -195,6 +204,9 @@ func (a *App) SyncTestConnection(serverURL, username, password string) error {
} }
func (a *App) SyncSetInterval(minutes int) error { func (a *App) SyncSetInterval(minutes int) error {
if err := a.requireVault(); err != nil {
return err
}
appCfg, _ := config.LoadAppConfig() appCfg, _ := config.LoadAppConfig()
if appCfg == nil { if appCfg == nil {
appCfg = config.DefaultAppConfig() appCfg = config.DefaultAppConfig()
@ -207,6 +219,9 @@ func (a *App) SyncSetInterval(minutes int) error {
} }
func (a *App) SyncNow() (map[string]interface{}, error) { func (a *App) SyncNow() (map[string]interface{}, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
serverURL, apiKey, lastPullSeq, _, err := a.sync.GetState() serverURL, apiKey, lastPullSeq, _, err := a.sync.GetState()
deviceToken := config.LoadDeviceToken(a.vault) deviceToken := config.LoadDeviceToken(a.vault)
if err != nil || serverURL == "" || (apiKey == "" && deviceToken == "") { if err != nil || serverURL == "" || (apiKey == "" && deviceToken == "") {
@ -316,6 +331,9 @@ func (a *App) updateSyncSuccess(lastSyncAt string) error {
// CheckSyncConnection tests the current sync connection. // CheckSyncConnection tests the current sync connection.
func (a *App) CheckSyncConnection() (bool, string) { func (a *App) CheckSyncConnection() (bool, string) {
if !a.IsReady() {
return false, "vault not open"
}
appCfg, _ := config.LoadAppConfig() appCfg, _ := config.LoadAppConfig()
if appCfg == nil || !appCfg.Vault.Sync.Enabled { if appCfg == nil || !appCfg.Vault.Sync.Enabled {
return false, "sync not configured" return false, "sync not configured"
@ -338,6 +356,9 @@ func (a *App) CheckSyncConnection() (bool, string) {
// ResetSyncKey clears the device token and resets sync state. // ResetSyncKey clears the device token and resets sync state.
func (a *App) ResetSyncKey() error { func (a *App) ResetSyncKey() error {
if err := a.requireVault(); err != nil {
return err
}
config.RemoveDeviceToken(a.vault) config.RemoveDeviceToken(a.vault)
appCfg, _ := config.LoadAppConfig() appCfg, _ := config.LoadAppConfig()
if appCfg == nil { if appCfg == nil {

View File

@ -11,6 +11,9 @@ import (
) )
func (a *App) ListWorklog(nodeID string) ([]WorklogDTO, error) { func (a *App) ListWorklog(nodeID string) ([]WorklogDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
list, err := a.worklog.ListByNode(nodeID) list, err := a.worklog.ListByNode(nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
@ -23,6 +26,9 @@ func (a *App) CreateWorklog(nodeID, summary string, minutes int) (*WorklogDTO, e
} }
func (a *App) CreateWorklogFull(nodeID, summary, details, date string, minutes int, approximate, billable bool) (*WorklogDTO, error) { func (a *App) CreateWorklogFull(nodeID, summary, details, date string, minutes int, approximate, billable bool) (*WorklogDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
if date == "" { if date == "" {
entry, err := a.worklog.Add(nodeID, summary, details, minutes, approximate, billable) entry, err := a.worklog.Add(nodeID, summary, details, minutes, approximate, billable)
if err != nil { if err != nil {
@ -42,6 +48,9 @@ func (a *App) CreateWorklogFull(nodeID, summary, details, date string, minutes i
// --- report bindings --- // --- report bindings ---
func (a *App) ListWorklogReport(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) ([]worklog.ReportRow, error) { func (a *App) ListWorklogReport(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) ([]worklog.ReportRow, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter) f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
rows, err := a.worklog.ListReport(f) rows, err := a.worklog.ListReport(f)
if err != nil { if err != nil {
@ -52,21 +61,33 @@ func (a *App) ListWorklogReport(dateFrom, dateTo, nodeID string, includeChildren
} }
func (a *App) WorklogReportSummary(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (*worklog.ReportSummary, error) { func (a *App) WorklogReportSummary(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (*worklog.ReportSummary, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter) f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
return a.worklog.Summary(f) return a.worklog.Summary(f)
} }
func (a *App) ExportWorklogCSV(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (string, error) { func (a *App) ExportWorklogCSV(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (string, error) {
if err := a.requireVault(); err != nil {
return "", err
}
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter) f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
return a.worklog.ExportCSV(f) return a.worklog.ExportCSV(f)
} }
func (a *App) ExportWorklogMarkdown(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (string, error) { func (a *App) ExportWorklogMarkdown(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (string, error) {
if err := a.requireVault(); err != nil {
return "", err
}
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter) f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
return a.worklog.ExportMarkdown(f) return a.worklog.ExportMarkdown(f)
} }
func (a *App) ExportWorklogPDF(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) ([]byte, error) { func (a *App) ExportWorklogPDF(dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) ([]byte, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter) f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
return a.worklog.ExportPDF(f) return a.worklog.ExportPDF(f)
} }
@ -97,6 +118,9 @@ func buildWorklogFilter(dateFrom, dateTo, nodeID string, includeChildren bool, b
// SaveWorklogReport generates a worklog report and opens a SaveFileDialog. // SaveWorklogReport generates a worklog report and opens a SaveFileDialog.
func (a *App) SaveWorklogReport(format, dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (string, error) { func (a *App) SaveWorklogReport(format, dateFrom, dateTo, nodeID string, includeChildren bool, billableFilter, approxFilter string) (string, error) {
if err := a.requireVault(); err != nil {
return "", err
}
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter) f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
var data []byte var data []byte
@ -159,6 +183,9 @@ func (a *App) SaveWorklogReport(format, dateFrom, dateTo, nodeID string, include
// GetWorklogEntryEvents returns activity events linked to a worklog entry. // GetWorklogEntryEvents returns activity events linked to a worklog entry.
func (a *App) GetWorklogEntryEvents(entryID string) ([]EventDTO, error) { func (a *App) GetWorklogEntryEvents(entryID string) ([]EventDTO, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
rows, err := a.db.Query( rows, err := a.db.Query(
`SELECT e.id, e.node_id, e.event_type, e.target_type, e.target_id, e.target_path, `SELECT e.id, e.node_id, e.event_type, e.target_type, e.target_id, e.target_path,
e.title, COALESCE(e.metadata,''), e.created_at e.title, COALESCE(e.metadata,''), e.created_at

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -16,8 +16,8 @@
background: #13131f; background: #13131f;
} }
</style> </style>
<script type="module" crossorigin src="/assets/main-DS67FqQ2.js"></script> <script type="module" crossorigin src="/assets/main-CDRB1gNP.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-oJnEtKWF.css"> <link rel="stylesheet" crossorigin href="/assets/main-BctNikp7.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@ -17,6 +17,9 @@ var assets embed.FS
func main() { func main() {
app := &App{} app := &App{}
// Fix WebKit signal handler for Go 1.24+ compatibility
ensureSignalOnStack()
err := wails.Run(&options.App{ err := wails.Run(&options.App{
Title: "Верстак", Title: "Верстак",
Width: 1280, Width: 1280,

38
cmd/verstak-gui/sigfix.go Normal file
View File

@ -0,0 +1,38 @@
package main
/*
#define _GNU_SOURCE
#include <signal.h>
#include <stdbool.h>
// fixSigsegvOnStack adds SA_ONSTACK to the current SIGSEGV handler.
// Go 1.24+ requires all signal handlers to have SA_ONSTACK set,
// but WebKit/JavaScriptCore installs a SIGSEGV handler without it.
void fixSigsegvOnStack(void) {
struct sigaction act;
if (sigaction(SIGSEGV, NULL, &act) == 0) {
if (!(act.sa_flags & SA_ONSTACK)) {
act.sa_flags |= SA_ONSTACK;
sigaction(SIGSEGV, &act, NULL);
}
}
}
*/
import "C"
import "time"
// ensureSignalOnStack periodically ensures SIGSEGV handler has SA_ONSTACK.
// This is needed because WebKit/JavaScriptCore installs a SIGSEGV handler
// without SA_ONSTACK, which causes Go 1.24+ to crash with:
// "non-Go code set up signal handler without SA_ONSTACK flag"
func ensureSignalOnStack() {
// Apply once after a short delay to let WebKit initialize
go func() {
// Retry a few times since WebKit may re-install its handler
for i := 0; i < 10; i++ {
time.Sleep(200 * time.Millisecond)
C.fixSigsegvOnStack()
}
}()
}

View File

@ -25,6 +25,9 @@ type VaultCheckResult struct {
} }
func (a *App) VaultCheck() (*VaultCheckResult, error) { func (a *App) VaultCheck() (*VaultCheckResult, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
result := &VaultCheckResult{Healthy: true} result := &VaultCheckResult{Healthy: true}
// Build a set of all node IDs for ancestor check // Build a set of all node IDs for ancestor check

View File

@ -13,6 +13,9 @@ import (
// parent-child relationships and creates human-readable folders in the vault. // parent-child relationships and creates human-readable folders in the vault.
// It performs a dry-run if dryRun is true. // It performs a dry-run if dryRun is true.
func (a *App) MigrateVaultLayout(dryRun bool) (*MigrationReport, error) { func (a *App) MigrateVaultLayout(dryRun bool) (*MigrationReport, error) {
if err := a.requireVault(); err != nil {
return nil, err
}
report := &MigrationReport{} report := &MigrationReport{}
// Load all nodes // Load all nodes

View File

@ -60,6 +60,12 @@
let caseActivity = [] let caseActivity = []
let version = '' let version = ''
let error = '' let error = ''
function translateError(msg) {
const map = {
'vault not open': t('error.vaultNotOpen'),
}
return map[msg] || msg
}
let selectedSection = '' let selectedSection = ''
let selectedNode = null let selectedNode = null
let activeTab = 'overview' let activeTab = 'overview'
@ -1518,8 +1524,8 @@
<SyncStatus {syncStatus} {syncLoading} onSync={runSyncNow} onOpenSettings={() => openSettings('sync')} /> <SyncStatus {syncStatus} {syncLoading} onSync={runSyncNow} onOpenSettings={() => openSettings('sync')} />
<div class="sidebar-footer-row"> <div class="sidebar-footer-row">
<button class="sidebar-settings-btn" on:click={() => openSettings()} title={t('common.settings')}> <button class="sidebar-settings-btn" on:click={() => openSettings()} title={t('common.settings')}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="2.8"/><path d="M12 1.5v1.9a1.8 1.8 0 0 0 1.6 1.77 1.8 1.8 0 0 0 1.74-.67l1.23-1.45a9 9 0 0 1 3.54 2.04l-.96 1.6a1.8 1.8 0 0 0 .2 2.08 1.8 1.8 0 0 0 1.98.49l1.75-.58a9 9 0 0 1 .68 4.09l-1.86.6a1.8 1.8 0 0 0-1.16 1.66 1.8 1.8 0 0 0 .93 1.6l.93.57a9 9 0 0 1-2.26 3.42l-1.32-1a1.8 1.8 0 0 0-2.1-.15 1.8 1.8 0 0 0-.87 1.55V22.5a9 9 0 0 1-4.1.01v-1.93a1.8 1.8 0 0 0-.93-1.56 1.8 1.8 0 0 0-2.1.16l-1.3.98a9 9 0 0 1-3.48-2.09l.92-1.54a1.8 1.8 0 0 0-.96-2.6 1.8 1.8 0 0 0-2.08.5l-.98 1.2a9 9 0 0 1-2.5-3.22l1.7-.67a1.8 1.8 0 0 0-1.7-2.51 1.8 1.8 0 0 0-.4.05L1.4 9.56a9 9 0 0 1 .22-4.12l1.72.68a1.8 1.8 0 0 0 2.1-.42 1.8 1.8 0 0 0 .22-2.03L4.6 2.34A9 9 0 0 1 8.84.38l.98 1.6a1.8 1.8 0 0 0 1.74.94A1.8 1.8 0 0 0 13 1.47V1.5z"/> <circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg> </svg>
</button> </button>
<span class="version">{version}</span> <span class="version">{version}</span>
@ -1541,17 +1547,12 @@
{/if} {/if}
</div> </div>
<div class="header-right"> <div class="header-right">
<button class="header-settings-btn" on:click={() => openSettings()} title={t('common.settings')}>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="2.8"/><path d="M12 1.5v1.9a1.8 1.8 0 0 0 1.6 1.77 1.8 1.8 0 0 0 1.74-.67l1.23-1.45a9 9 0 0 1 3.54 2.04l-.96 1.6a1.8 1.8 0 0 0 .2 2.08 1.8 1.8 0 0 0 1.98.49l1.75-.58a9 9 0 0 1 .68 4.09l-1.86.6a1.8 1.8 0 0 0-1.16 1.66 1.8 1.8 0 0 0 .93 1.6l.93.57a9 9 0 0 1-2.26 3.42l-1.32-1a1.8 1.8 0 0 0-2.1-.15 1.8 1.8 0 0 0-.87 1.55V22.5a9 9 0 0 1-4.1.01v-1.93a1.8 1.8 0 0 0-.93-1.56 1.8 1.8 0 0 0-2.1.16l-1.3.98a9 9 0 0 1-3.48-2.09l.92-1.54a1.8 1.8 0 0 0-.96-2.6 1.8 1.8 0 0 0-2.08.5l-.98 1.2a9 9 0 0 1-2.5-3.22l1.7-.67a1.8 1.8 0 0 0-1.7-2.51 1.8 1.8 0 0 0-.4.05L1.4 9.56a9 9 0 0 1 .22-4.12l1.72.68a1.8 1.8 0 0 0 2.1-.42 1.8 1.8 0 0 0 .22-2.03L4.6 2.34A9 9 0 0 1 8.84.38l.98 1.6a1.8 1.8 0 0 0 1.74.94A1.8 1.8 0 0 0 13 1.47V1.5z"/>
</svg>
</button>
</div> </div>
</header> </header>
{#if error} {#if error}
<div class="error-banner" role="button" tabindex="0" on:click={() => error = ''} on:keydown={onKeyActivate(() => error = '')}> <div class="error-banner" role="button" tabindex="0" on:click={() => error = ''} on:keydown={onKeyActivate(() => error = '')}>
{error} {translateError(error)}
<button class="dismiss-btn" on:click|stopPropagation={() => error = ''} aria-label="Dismiss"> <button class="dismiss-btn" on:click|stopPropagation={() => error = ''} aria-label="Dismiss">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
@ -2512,9 +2513,7 @@
.sidebar-settings-btn { background: transparent; border: none; border-radius: 6px; padding: 6px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; color: #666; font-family: inherit; width: 32px; height: 32px; } .sidebar-settings-btn { background: transparent; border: none; border-radius: 6px; padding: 6px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; color: #666; font-family: inherit; width: 32px; height: 32px; }
.sidebar-settings-btn:hover { background: #1e1e38; color: #a5b4fc; } .sidebar-settings-btn:hover { background: #1e1e38; color: #a5b4fc; }
.sidebar-settings-btn:active { background: #252545; color: #818cf8; } .sidebar-settings-btn:active { background: #252545; color: #818cf8; }
.header-settings-btn { background: transparent; border: none; border-radius: 6px; padding: 6px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; color: #666; font-family: inherit; width: 32px; height: 32px; }
.header-settings-btn:hover { background: #1e1e38; color: #a5b4fc; }
.header-settings-btn:active { background: #252545; color: #818cf8; }
.crumb { font-size: 14px; font-weight: 500; } .crumb { font-size: 14px; font-weight: 500; }
.crumb.placeholder { color: #666; } .crumb.placeholder { color: #666; }
.crumb-type { font-size: 11px; color: #555; background: #1e1e2e; padding: 2px 8px; border-radius: 10px; margin-left: 8px; } .crumb-type { font-size: 11px; color: #555; background: #1e1e2e; padding: 2px 8px; border-radius: 10px; margin-left: 8px; }

View File

@ -120,6 +120,7 @@ export default {
'error.generic': 'An error occurred', 'error.generic': 'An error occurred',
'error.invalidCredentials': 'Invalid username or password', 'error.invalidCredentials': 'Invalid username or password',
'error.vaultNotOpen': 'Vault not open',
'worklog.suggestions': 'Suggestions for today', 'worklog.suggestions': 'Suggestions for today',
'worklog.apply': 'Apply', 'worklog.apply': 'Apply',

View File

@ -325,6 +325,7 @@ export default {
'error.nameEmpty': 'Имя не может быть пустым', 'error.nameEmpty': 'Имя не может быть пустым',
'error.nameInvalid': 'Недопустимое имя', 'error.nameInvalid': 'Недопустимое имя',
'error.selectCaseFirst': 'Сначала выберите дело', 'error.selectCaseFirst': 'Сначала выберите дело',
'error.vaultNotOpen': 'Хранилище не открыто',
'common.open': 'Открыть', 'common.open': 'Открыть',
'delete.files': 'файлов ({count})', 'delete.files': 'файлов ({count})',
'file.namePrompt': 'Введите имя файла:', 'file.namePrompt': 'Введите имя файла:',