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 (
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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': 'Введите имя файла:',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue