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:
parent
f92394e3d7
commit
a69dc845e6
|
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
|
@ -44,6 +45,15 @@ type App struct {
|
|||
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.
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import (
|
|||
)
|
||||
|
||||
func (a *App) ListActions(nodeID string) ([]ActionDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := a.actions.ListByNode(nodeID)
|
||||
if err != nil {
|
||||
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) {
|
||||
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)
|
||||
if err != nil {
|
||||
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 {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = a.sync.RecordOp(syncsvc.EntityAction, id, syncsvc.OpDelete, nil)
|
||||
return a.actions.Delete(id)
|
||||
}
|
||||
|
||||
func (a *App) RunAction(id string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := a.actions.Run(id)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ func (a *App) ListSystemViews() []SystemViewDTO {
|
|||
}
|
||||
|
||||
func (a *App) ListTodayView() (*TodayDashboardDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aeByParent, err := a.activity.ListTodayEventsByParent()
|
||||
if err != nil {
|
||||
aeByParent = nil
|
||||
|
|
@ -144,6 +147,9 @@ func (a *App) ListTodayView() (*TodayDashboardDTO, 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)
|
||||
if err != nil {
|
||||
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) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events, err := a.activity.ListByNode(nodeID, limit, offset)
|
||||
if err != nil {
|
||||
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) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return a.activity.CountByNode(nodeID)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,16 @@ func (a *App) GetStartupStatus() (*StartupStatus, error) {
|
|||
}, 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,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import (
|
|||
// WriteDebugLog appends a line to <vault>/.verstak/debug.log.
|
||||
// Called from frontend to log JS-side diagnostics in production GUI builds.
|
||||
func (a *App) WriteDebugLog(msg string) {
|
||||
if !a.IsReady() {
|
||||
return
|
||||
}
|
||||
logPath := filepath.Join(a.vault, ".verstak", "debug.log")
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@ import (
|
|||
)
|
||||
|
||||
func (a *App) ListFiles(nodeID string) ([]FileDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records, err := a.files.ListByNode(nodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -30,6 +33,9 @@ func (a *App) ListFiles(nodeID string) ([]FileDTO, 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -61,6 +67,9 @@ func (a *App) ListItems(nodeID string) ([]FileTreeItemDTO, 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)
|
||||
if err != nil {
|
||||
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) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes, err := a.files.AddPathLink(nodeID, sourcePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -85,6 +97,9 @@ func (a *App) AddPathLink(nodeID, sourcePath string) ([]NodeDTO, error) {
|
|||
}
|
||||
|
||||
func (a *App) DeleteFileOrFolder(nodeID string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := a.nodes.GetActive(nodeID)
|
||||
if err == nil {
|
||||
pid := ""
|
||||
|
|
@ -108,6 +123,9 @@ func (a *App) DeleteFileOrFolder(nodeID string) 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -119,6 +137,9 @@ func (a *App) CreateEmptyFile(parentID, filename 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -139,5 +160,8 @@ func (a *App) ValidateName(name string) error {
|
|||
}
|
||||
|
||||
func (a *App) PreviewImport(sourcePath string) (*files.ImportSummary, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.files.PreviewImport(sourcePath)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import (
|
|||
)
|
||||
|
||||
func (a *App) ListWorkspaceTree() ([]NodeDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := a.nodes.ListRoots(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -31,6 +34,9 @@ func (a *App) ListWorkspaceTree() ([]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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -65,6 +71,9 @@ func isContainerType(typ string) bool {
|
|||
}
|
||||
|
||||
func (a *App) ListChildren(parentID string) ([]NodeDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := a.nodes.ListChildren(parentID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -73,6 +82,9 @@ func (a *App) ListChildren(parentID 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -82,6 +94,9 @@ func (a *App) GetNodeDetail(nodeID string) (*NodeDTO, error) {
|
|||
}
|
||||
|
||||
func (a *App) GetNodeTitle(nodeID string) (string, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
n, err := a.nodes.GetActive(nodeID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
@ -90,6 +105,9 @@ func (a *App) GetNodeTitle(nodeID string) (string, 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)
|
||||
if !ok {
|
||||
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 {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := a.nodes.GetActive(id)
|
||||
if err != nil {
|
||||
return a.nodes.SoftDelete(id)
|
||||
|
|
@ -282,6 +303,9 @@ func (a *App) DeleteNode(id string) error {
|
|||
}
|
||||
|
||||
func (a *App) RenameNode(nodeID, newTitle string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := a.nodes.GetActive(nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -574,6 +598,9 @@ func (a *App) wouldCreateCycle(nodeID, newParentID string) error {
|
|||
}
|
||||
|
||||
func (a *App) MoveNode(nodeID, newParentID string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
if nodeID == "" {
|
||||
return fmt.Errorf("node ID is required")
|
||||
}
|
||||
|
|
@ -918,6 +945,9 @@ type SearchNodeResult struct {
|
|||
}
|
||||
|
||||
func (a *App) SearchNodes(query string) ([]SearchNodeResult, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes, err := a.nodes.Search(query, 20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -939,6 +969,9 @@ func (a *App) SearchNodes(query string) ([]SearchNodeResult, error) {
|
|||
}
|
||||
|
||||
func (a *App) OpenNodeFolder(nodeID string) (string, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
n, err := a.nodes.GetActive(nodeID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import (
|
|||
)
|
||||
|
||||
func (a *App) ListNotes(nodeID string) ([]NodeDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
children, err := a.nodes.ListChildren(nodeID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -23,6 +26,9 @@ func (a *App) ListNotes(nodeID 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, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -34,10 +40,16 @@ func (a *App) CreateNote(parentID, title string) (*NodeDTO, error) {
|
|||
}
|
||||
|
||||
func (a *App) ReadNote(noteID string) (string, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return a.notes.Read(noteID)
|
||||
}
|
||||
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ func (a *App) SetTemplateEnabled(templateID string, enabled bool) error {
|
|||
}
|
||||
|
||||
func (a *App) ListTemplates() []TemplateDTO {
|
||||
if !a.IsReady() {
|
||||
return nil
|
||||
}
|
||||
templates := a.plugins.Templates()
|
||||
out := make([]TemplateDTO, 0, len(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) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var tmpl *plugins.TemplateDefinition
|
||||
for _, t := range a.plugins.Templates() {
|
||||
if t.Name == template {
|
||||
|
|
@ -166,18 +172,30 @@ func (a *App) PickDirectory() (string, error) {
|
|||
}
|
||||
|
||||
func (a *App) OpenFile(fileID string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
return a.files.Open(fileID)
|
||||
}
|
||||
|
||||
func (a *App) ReadFileText(fileID string) (string, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return a.files.ReadText(fileID)
|
||||
}
|
||||
|
||||
func (a *App) GetFileBase64(fileID string) (string, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return a.files.ReadBase64(fileID)
|
||||
}
|
||||
|
||||
func (a *App) OpenFolder(nodeID string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := a.nodes.GetActive(nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -214,6 +232,9 @@ func (a *App) OpenVaultFolder() error {
|
|||
}
|
||||
|
||||
func (a *App) Search(query string) ([]SearchResultDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if query == "" {
|
||||
return []SearchResultDTO{}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import (
|
|||
// GetSuggestions analyzes today's activity and returns conservative suggestions.
|
||||
// Only events not already linked in worklog_entry_events are considered.
|
||||
func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events, err := a.activity.ListTodayEvents()
|
||||
if err != nil || len(events) == 0 {
|
||||
return nil, err
|
||||
|
|
@ -114,12 +117,18 @@ func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
|
|||
|
||||
// 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) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.AcceptSuggestionWith(nodeID, summary, minutes, date, eventIDsJSON)
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
if d == "" {
|
||||
d = time.Now().Format("2006-01-02")
|
||||
|
|
|
|||
|
|
@ -114,6 +114,9 @@ type SyncSettingsDTO struct {
|
|||
}
|
||||
|
||||
func (a *App) GetSyncSettings() (*SyncSettingsDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
appCfg = config.DefaultAppConfig()
|
||||
|
|
@ -133,6 +136,9 @@ func (a *App) GetSyncSettings() (*SyncSettingsDTO, error) {
|
|||
}
|
||||
|
||||
func (a *App) SyncConfigure(serverURL, username, password string) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
hostname, _ := os.Hostname()
|
||||
if hostname == "" {
|
||||
hostname = "unknown"
|
||||
|
|
@ -165,6 +171,9 @@ func (a *App) SyncConfigure(serverURL, username, password string) error {
|
|||
}
|
||||
|
||||
func (a *App) SyncDisconnect() error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
deviceToken := config.LoadDeviceToken(a.vault)
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
|
|
@ -195,6 +204,9 @@ func (a *App) SyncTestConnection(serverURL, username, password string) error {
|
|||
}
|
||||
|
||||
func (a *App) SyncSetInterval(minutes int) error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
appCfg = config.DefaultAppConfig()
|
||||
|
|
@ -207,6 +219,9 @@ func (a *App) SyncSetInterval(minutes int) 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()
|
||||
deviceToken := config.LoadDeviceToken(a.vault)
|
||||
if err != nil || serverURL == "" || (apiKey == "" && deviceToken == "") {
|
||||
|
|
@ -316,6 +331,9 @@ func (a *App) updateSyncSuccess(lastSyncAt string) error {
|
|||
|
||||
// CheckSyncConnection tests the current sync connection.
|
||||
func (a *App) CheckSyncConnection() (bool, string) {
|
||||
if !a.IsReady() {
|
||||
return false, "vault not open"
|
||||
}
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil || !appCfg.Vault.Sync.Enabled {
|
||||
return false, "sync not configured"
|
||||
|
|
@ -338,6 +356,9 @@ func (a *App) CheckSyncConnection() (bool, string) {
|
|||
|
||||
// ResetSyncKey clears the device token and resets sync state.
|
||||
func (a *App) ResetSyncKey() error {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return err
|
||||
}
|
||||
config.RemoveDeviceToken(a.vault)
|
||||
appCfg, _ := config.LoadAppConfig()
|
||||
if appCfg == nil {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import (
|
|||
)
|
||||
|
||||
func (a *App) ListWorklog(nodeID string) ([]WorklogDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list, err := a.worklog.ListByNode(nodeID)
|
||||
if err != nil {
|
||||
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) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if date == "" {
|
||||
entry, err := a.worklog.Add(nodeID, summary, details, minutes, approximate, billable)
|
||||
if err != nil {
|
||||
|
|
@ -42,6 +48,9 @@ func (a *App) CreateWorklogFull(nodeID, summary, details, date string, minutes i
|
|||
// --- report bindings ---
|
||||
|
||||
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)
|
||||
rows, err := a.worklog.ListReport(f)
|
||||
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) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := buildWorklogFilter(dateFrom, dateTo, nodeID, includeChildren, billableFilter, approxFilter)
|
||||
return a.worklog.Summary(f)
|
||||
}
|
||||
|
||||
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)
|
||||
return a.worklog.ExportCSV(f)
|
||||
}
|
||||
|
||||
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)
|
||||
return a.worklog.ExportMarkdown(f)
|
||||
}
|
||||
|
||||
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)
|
||||
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.
|
||||
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)
|
||||
|
||||
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.
|
||||
func (a *App) GetWorklogEntryEvents(entryID string) ([]EventDTO, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := a.db.Query(
|
||||
`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
|
||||
|
|
|
|||
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
|
|
@ -16,8 +16,8 @@
|
|||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-DS67FqQ2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-oJnEtKWF.css">
|
||||
<script type="module" crossorigin src="/assets/main-CDRB1gNP.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-BctNikp7.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ var assets embed.FS
|
|||
func main() {
|
||||
app := &App{}
|
||||
|
||||
// Fix WebKit signal handler for Go 1.24+ compatibility
|
||||
ensureSignalOnStack()
|
||||
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Верстак",
|
||||
Width: 1280,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -25,6 +25,9 @@ type VaultCheckResult struct {
|
|||
}
|
||||
|
||||
func (a *App) VaultCheck() (*VaultCheckResult, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &VaultCheckResult{Healthy: true}
|
||||
|
||||
// Build a set of all node IDs for ancestor check
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import (
|
|||
// parent-child relationships and creates human-readable folders in the vault.
|
||||
// It performs a dry-run if dryRun is true.
|
||||
func (a *App) MigrateVaultLayout(dryRun bool) (*MigrationReport, error) {
|
||||
if err := a.requireVault(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
report := &MigrationReport{}
|
||||
|
||||
// Load all nodes
|
||||
|
|
|
|||
|
|
@ -60,6 +60,12 @@
|
|||
let caseActivity = []
|
||||
let version = ''
|
||||
let error = ''
|
||||
function translateError(msg) {
|
||||
const map = {
|
||||
'vault not open': t('error.vaultNotOpen'),
|
||||
}
|
||||
return map[msg] || msg
|
||||
}
|
||||
let selectedSection = ''
|
||||
let selectedNode = null
|
||||
let activeTab = 'overview'
|
||||
|
|
@ -1518,8 +1524,8 @@
|
|||
<SyncStatus {syncStatus} {syncLoading} onSync={runSyncNow} onOpenSettings={() => openSettings('sync')} />
|
||||
<div class="sidebar-footer-row">
|
||||
<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">
|
||||
<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 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="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>
|
||||
</button>
|
||||
<span class="version">{version}</span>
|
||||
|
|
@ -1541,17 +1547,12 @@
|
|||
{/if}
|
||||
</div>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
{#if 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">
|
||||
<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"/>
|
||||
|
|
@ -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:hover { background: #1e1e38; color: #a5b4fc; }
|
||||
.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.placeholder { color: #666; }
|
||||
.crumb-type { font-size: 11px; color: #555; background: #1e1e2e; padding: 2px 8px; border-radius: 10px; margin-left: 8px; }
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ export default {
|
|||
|
||||
'error.generic': 'An error occurred',
|
||||
'error.invalidCredentials': 'Invalid username or password',
|
||||
'error.vaultNotOpen': 'Vault not open',
|
||||
|
||||
'worklog.suggestions': 'Suggestions for today',
|
||||
'worklog.apply': 'Apply',
|
||||
|
|
|
|||
|
|
@ -325,6 +325,7 @@ export default {
|
|||
'error.nameEmpty': 'Имя не может быть пустым',
|
||||
'error.nameInvalid': 'Недопустимое имя',
|
||||
'error.selectCaseFirst': 'Сначала выберите дело',
|
||||
'error.vaultNotOpen': 'Хранилище не открыто',
|
||||
'common.open': 'Открыть',
|
||||
'delete.files': 'файлов ({count})',
|
||||
'file.namePrompt': 'Введите имя файла:',
|
||||
|
|
|
|||
Loading…
Reference in New Issue