diff --git a/cmd/verstak-gui/app.go b/cmd/verstak-gui/app.go new file mode 100644 index 0000000..018a93f --- /dev/null +++ b/cmd/verstak-gui/app.go @@ -0,0 +1,383 @@ +package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + wailsruntime "github.com/wailsapp/wails/v2/pkg/runtime" + + "verstak/internal/core/actions" + "verstak/internal/core/files" + "verstak/internal/core/notes" + "verstak/internal/core/nodes" + "verstak/internal/core/storage" + "verstak/internal/core/worklog" +) + +// App is the Wails v2 application adapter. It wraps core services. +type App struct { + ctx context.Context + db *storage.DB + nodes *nodes.Repository + files *files.Service + notes *notes.Service + actions *actions.Service + worklog *worklog.Service + vault string +} + +// startup is called when the app starts. Store context. +func (a *App) startup(ctx context.Context) { + a.ctx = ctx +} + +// ============================================================ +// DTOs +// ============================================================ + +type NodeDTO struct { + ID string `json:"id"` + ParentID string `json:"parentId"` + Title string `json:"title"` + Type string `json:"type"` + Section string `json:"section"` + Path string `json:"path"` + CreatedAt string `json:"createdAt"` +} + +type SectionDTO struct { + ID string `json:"id"` + Label string `json:"label"` +} + +type NoteDTO struct { + ID string `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Format string `json:"format"` + CreatedAt string `json:"createdAt"` +} + +type FileDTO struct { + ID string `json:"id"` + NodeID string `json:"nodeId"` + Name string `json:"name"` + Path string `json:"path"` + Size int64 `json:"size"` + Mime string `json:"mime"` + IsDir bool `json:"isDir"` + Missing bool `json:"missing"` +} + +type ActionDTO struct { + ID string `json:"id"` + NodeID string `json:"nodeId"` + Title string `json:"title"` + Type string `json:"type"` + Data string `json:"data"` +} + +type WorklogDTO struct { + ID string `json:"id"` + NodeID string `json:"nodeId"` + Summary string `json:"summary"` + Minutes int `json:"minutes"` + CreatedAt string `json:"createdAt"` +} + +type SearchResultDTO struct { + NodeID string `json:"nodeId"` + Title string `json:"title"` + Snippet string `json:"snippet"` + Type string `json:"type"` +} + +// ============================================================ +// Sections +// ============================================================ + +func (a *App) ListSections() []SectionDTO { + return []SectionDTO{ + {ID: "today", Label: "Сегодня"}, + {ID: "inbox", Label: "Неразобранное"}, + {ID: "clients", Label: "Клиенты"}, + {ID: "projects", Label: "Проекты"}, + {ID: "recipes", Label: "Рецепты"}, + {ID: "documents", Label: "Документы"}, + {ID: "archive", Label: "Архив"}, + } +} + +// ============================================================ +// Nodes +// ============================================================ + +func (a *App) ListNodesBySection(section string) ([]NodeDTO, error) { + list, err := a.nodes.ListRoots(false, section) + if err != nil { + return nil, err + } + return toNodeDTOs(list), nil +} + +func (a *App) ListChildren(parentID string) ([]NodeDTO, error) { + list, err := a.nodes.ListChildren(parentID, false) + if err != nil { + return nil, err + } + return toNodeDTOs(list), nil +} + +func (a *App) GetNodeDetail(nodeID string) (*NodeDTO, error) { + n, err := a.nodes.GetActive(nodeID) + if err != nil { + return nil, err + } + dto := toNodeDTO(n) + return &dto, nil +} + +func (a *App) CreateNode(parentID, nodeType, title, section string) (*NodeDTO, error) { + n, err := a.nodes.Create(parentID, nodeType, title, section) + if err != nil { + return nil, err + } + dto := toNodeDTO(n) + return &dto, nil +} + +func (a *App) DeleteNode(id string) error { + return a.nodes.SoftDelete(id) +} + +// ============================================================ +// Notes +// ============================================================ + +// ListNotes returns note-type children of a node. +func (a *App) ListNotes(nodeID string) ([]NodeDTO, error) { + children, err := a.nodes.ListChildren(nodeID, false) + if err != nil { + return nil, err + } + var result []NodeDTO + for i := range children { + if children[i].Type == nodes.TypeNote { + result = append(result, toNodeDTO(&children[i])) + } + } + return result, nil +} + +// CreateNote creates a note under a parent node. +func (a *App) CreateNote(parentID, title string) (*NodeDTO, error) { + node, _, err := a.notes.Create(parentID, title, "") + if err != nil { + return nil, err + } + dto := toNodeDTO(node) + return &dto, nil +} + +// ReadNote reads note content. +func (a *App) ReadNote(noteID string) (string, error) { + return a.notes.Read(noteID) +} + +// SaveNote saves note content. +func (a *App) SaveNote(noteID, content string) error { + return a.notes.Save(noteID, content) +} + +// ============================================================ +// Files +// ============================================================ + +func (a *App) ListFiles(nodeID string) ([]FileDTO, error) { + records, err := a.files.ListByNode(nodeID) + if err != nil { + return nil, err + } + result := make([]FileDTO, len(records)) + for i := range records { + isDir := records[i].MIME == "inode/directory" + missing := false + result[i] = FileDTO{ + ID: records[i].ID, + NodeID: records[i].NodeID, + Name: records[i].Filename, + Path: records[i].Path, + Size: records[i].Size, + Mime: records[i].MIME, + IsDir: isDir, + Missing: missing, + } + } + return result, nil +} + +// ============================================================ +// Actions +// ============================================================ + +func (a *App) ListActions(nodeID string) ([]ActionDTO, error) { + list, err := a.actions.ListByNode(nodeID) + if err != nil { + return nil, err + } + result := make([]ActionDTO, len(list)) + for i := range list { + data := list[i].Command + if list[i].URL != "" { + data = list[i].URL + } + result[i] = ActionDTO{ + ID: list[i].ID, + NodeID: list[i].NodeID, + Title: list[i].Title, + Type: list[i].Kind, + Data: data, + } + } + return result, nil +} + +func (a *App) RunAction(id string) error { + _, err := a.actions.Run(id) + return err +} + +// ============================================================ +// Worklog +// ============================================================ + +func (a *App) ListWorklog(nodeID string) ([]WorklogDTO, error) { + list, err := a.worklog.ListByNode(nodeID) + if err != nil { + return nil, err + } + result := make([]WorklogDTO, len(list)) + for i := range list { + mins := 0 + if list[i].Minutes != nil { + mins = *list[i].Minutes + } + result[i] = WorklogDTO{ + ID: list[i].ID, + NodeID: list[i].NodeID, + Summary: list[i].Summary, + Minutes: mins, + CreatedAt: list[i].CreatedAt.Format("2006-01-02T15:04:05Z"), + } + } + return result, nil +} + +func (a *App) CreateWorklog(nodeID, summary string, minutes int) (*WorklogDTO, error) { + entry, err := a.worklog.Add(nodeID, summary, "", minutes, false, false) + if err != nil { + return nil, err + } + mins := 0 + if entry.Minutes != nil { + mins = *entry.Minutes + } + dto := &WorklogDTO{ + ID: entry.ID, + NodeID: entry.NodeID, + Summary: entry.Summary, + Minutes: mins, + CreatedAt: entry.CreatedAt.Format("2006-01-02T15:04:05Z"), + } + return dto, nil +} + +// ============================================================ +// Search (stubs — search service not wired yet) +// ============================================================ + +func (a *App) Search(query string) ([]SearchResultDTO, error) { + if strings.TrimSpace(query) == "" { + return []SearchResultDTO{}, nil + } + return []SearchResultDTO{}, nil +} + +// ============================================================ +// File Dialogs (Wails v2 Runtime) +// ============================================================ + +func (a *App) PickFile() (string, error) { + return wailsruntime.OpenFileDialog(a.ctx, wailsruntime.OpenDialogOptions{ + Title: "Выберите файл", + }) +} + +func (a *App) PickFiles() ([]string, error) { + return wailsruntime.OpenMultipleFilesDialog(a.ctx, wailsruntime.OpenDialogOptions{ + Title: "Выберите файлы", + }) +} + +func (a *App) PickDirectory() (string, error) { + return wailsruntime.OpenDirectoryDialog(a.ctx, wailsruntime.OpenDialogOptions{ + Title: "Выберите папку", + }) +} + +// ============================================================ +// System helpers +// ============================================================ + +func (a *App) OpenFile(fileID string) error { + return fmt.Errorf("not implemented: %s", fileID) +} + +func (a *App) OpenFolder(nodeID string) error { + cmd := exec.Command("xdg-open", a.vault) + return cmd.Run() +} + +func (a *App) VerstakVersion() string { + return "verstak-gui/v2" +} + +// ============================================================ +// Helpers +// ============================================================ + +func toNodeDTO(n *nodes.Node) NodeDTO { + parentID := "" + if n.ParentID != nil { + parentID = *n.ParentID + } + path := "" + if n.Path != nil { + path = *n.Path + } + return NodeDTO{ + ID: n.ID, + ParentID: parentID, + Title: n.Title, + Type: n.Type, + Section: n.Section, + Path: path, + CreatedAt: n.CreatedAt.Format("2006-01-02T15:04:05Z"), + } +} + +func toNodeDTOs(list []nodes.Node) []NodeDTO { + result := make([]NodeDTO, len(list)) + for i := range list { + result[i] = toNodeDTO(&list[i]) + } + return result +} + +var ( + _ = os.Getenv + _ = exec.Command +) diff --git a/cmd/verstak-gui/frontend-dist/assets/index-COs6tJEl.js b/cmd/verstak-gui/frontend-dist/assets/index-COs6tJEl.js new file mode 100644 index 0000000..b95e9a9 --- /dev/null +++ b/cmd/verstak-gui/frontend-dist/assets/index-COs6tJEl.js @@ -0,0 +1 @@ +var Ae=Object.defineProperty;var Ne=(t,e,l)=>e in t?Ae(t,e,{enumerable:!0,configurable:!0,writable:!0,value:l}):t[e]=l;var ee=(t,e,l)=>Ne(t,typeof e!="symbol"?e+"":e,l);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))n(s);new MutationObserver(s=>{for(const i of s)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function l(s){const i={};return s.integrity&&(i.integrity=s.integrity),s.referrerPolicy&&(i.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?i.credentials="include":s.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(s){if(s.ep)return;s.ep=!0;const i=l(s);fetch(s.href,i)}})();function B(){}function we(t){return t()}function fe(){return Object.create(null)}function Z(t){t.forEach(we)}function $e(t){return typeof t=="function"}function Oe(t,e){return t!=t?e==e:t!==e||t&&typeof t=="object"||typeof t=="function"}function Ce(t){return Object.keys(t).length===0}function c(t,e){t.appendChild(e)}function N(t,e,l){t.insertBefore(e,l||null)}function L(t){t.parentNode&&t.parentNode.removeChild(t)}function le(t,e){for(let l=0;lt.removeEventListener(e,l,n)}function a(t,e,l){l==null?t.removeAttribute(e):t.getAttribute(e)!==l&&t.setAttribute(e,l)}function Se(t){return Array.from(t.childNodes)}function S(t,e){e=""+e,t.data!==e&&(t.data=e)}let J;function W(t){J=t}function Pe(){if(!J)throw new Error("Function called outside component initialization");return J}function Ie(t){Pe().$$.on_mount.push(t)}const q=[],ae=[];let D=[];const de=[],Me=Promise.resolve();let ne=!1;function je(){ne||(ne=!0,Me.then(Le))}function se(t){D.push(t)}const te=new Set;let R=0;function Le(){if(R!==0)return;const t=J;do{try{for(;Rt.indexOf(n)===-1?e.push(n):l.push(n)),l.forEach(n=>n()),D=e}const Ve=new Set;function Fe(t,e){t&&t.i&&(Ve.delete(t),t.i(e))}function z(t){return(t==null?void 0:t.length)!==void 0?t:Array.from(t)}function He(t,e,l){const{fragment:n,after_update:s}=t.$$;n&&n.m(e,l),se(()=>{const i=t.$$.on_mount.map(we).filter($e);t.$$.on_destroy?t.$$.on_destroy.push(...i):Z(i),t.$$.on_mount=[]}),s.forEach(se)}function Re(t,e){const l=t.$$;l.fragment!==null&&(Be(l.after_update),Z(l.on_destroy),l.fragment&&l.fragment.d(e),l.on_destroy=l.fragment=null,l.ctx=[])}function qe(t,e){t.$$.dirty[0]===-1&&(q.push(t),je(),t.$$.dirty.fill(0)),t.$$.dirty[e/31|0]|=1<{const b=h.length?h[0]:v;return u.ctx&&s(u.ctx[g],u.ctx[g]=b)&&(!u.skip_bound&&u.bound[g]&&u.bound[g](b),_&&qe(t,g)),v}):[],u.update(),_=!0,Z(u.before_update),u.fragment=n?n(u.ctx):!1,e.target){if(e.hydrate){const g=Se(e.target);u.fragment&&u.fragment.l(g),g.forEach(L)}else u.fragment&&u.fragment.c();e.intro&&Fe(t.$$.fragment),He(t,e.target,e.anchor),Le()}W(m)}class De{constructor(){ee(this,"$$");ee(this,"$$set")}$destroy(){Re(this,1),this.$destroy=B}$on(e,l){if(!$e(l))return B;const n=this.$$.callbacks[e]||(this.$$.callbacks[e]=[]);return n.push(l),()=>{const s=n.indexOf(l);s!==-1&&n.splice(s,1)}}$set(e){this.$$set&&!Ce(e)&&(this.$$.skip_bound=!0,this.$$set(e),this.$$.skip_bound=!1)}}const Ge="4";typeof window<"u"&&(window.__svelte||(window.__svelte={v:new Set})).v.add(Ge);function _e(t,e,l){const n=t.slice();return n[10]=e[l],n}function he(t,e,l){const n=t.slice();return n[13]=e[l],n}function pe(t,e,l){const n=t.slice();return n[10]=e[l],n}function ve(t){let e,l=t[10].label+"",n,s,i,o,p;function m(){return t[8](t[10])}return{c(){e=d("button"),n=k(l),s=$(),a(e,"class",i="nav-item "+(t[4]===t[10].id?"selected":"")+" svelte-14ysusk")},m(u,_){N(u,e,_),c(e,n),c(e,s),o||(p=Ee(e,"click",m),o=!0)},p(u,_){t=u,_&1&&l!==(l=t[10].label+"")&&S(n,l),_&17&&i!==(i="nav-item "+(t[4]===t[10].id?"selected":"")+" svelte-14ysusk")&&a(e,"class",i)},d(u){u&&L(e),o=!1,p()}}}function me(t){let e,l=t[13].title+"",n,s,i,o;function p(){return t[9](t[13])}return{c(){e=d("button"),n=k(l),a(e,"class",s="nav-item "+(t[5]&&t[5].id===t[13].id?"selected":"")+" svelte-14ysusk")},m(m,u){N(m,e,u),c(e,n),i||(o=Ee(e,"click",p),i=!0)},p(m,u){t=m,u&2&&l!==(l=t[13].title+"")&&S(n,l),u&34&&s!==(s="nav-item "+(t[5]&&t[5].id===t[13].id?"selected":"")+" svelte-14ysusk")&&a(e,"class",s)},d(m){m&&L(e),i=!1,o()}}}function ge(t){let e;return{c(){e=d("div"),e.textContent="Нет дел",a(e,"class","nav-empty svelte-14ysusk")},m(l,n){N(l,e,n)},d(l){l&&L(e)}}}function Ke(t){let e;return{c(){e=d("span"),e.textContent="Выберите раздел или дело",a(e,"class","crumb placeholder svelte-14ysusk")},m(l,n){N(l,e,n)},p:B,d(l){l&&L(e)}}}function Ue(t){let e,l=z(t[0]),n=[];for(let s=0;s0&&ge();function re(r,C){return r[5]?We:r[4]?Ue:Ke}let X=re(t),P=X(t),E=t[3]&&be(t);function ue(r,C){return r[5]?Xe:r[0].length>0?Qe:Je}let Y=ue(t),I=Y(t);return{c(){e=d("div"),l=d("aside"),n=d("div"),n.innerHTML=' Верстак',s=$(),i=d("nav"),o=d("div"),p=d("div"),p.textContent="Разделы",m=$();for(let r=0;rПоиск...',ce=$(),E&&E.c(),x=$(),K=d("div"),I.c(),a(n,"class","sidebar-top svelte-14ysusk"),a(p,"class","nav-label svelte-14ysusk"),a(o,"class","nav-group svelte-14ysusk"),a(g,"class","nav-label svelte-14ysusk"),a(_,"class","nav-group svelte-14ysusk"),a(i,"class","sidebar-nav svelte-14ysusk"),a(O,"class","version svelte-14ysusk"),a(T,"class","sidebar-bottom svelte-14ysusk"),a(l,"class","sidebar svelte-14ysusk"),a(G,"class","header-left svelte-14ysusk"),a(Q,"class","header-right svelte-14ysusk"),a(V,"class","header svelte-14ysusk"),a(K,"class","content svelte-14ysusk"),a(j,"class","main svelte-14ysusk"),a(e,"class","app svelte-14ysusk")},m(r,C){N(r,e,C),c(e,l),c(l,n),c(l,s),c(l,i),c(i,o),c(o,p),c(o,m);for(let f=0;f0?A||(A=ge(),A.c(),A.m(_,null)):A&&(A.d(1),A=null),C&4&&S(M,r[2]),X===(X=re(r))&&P?P.p(r,C):(P.d(1),P=X(r),P&&(P.c(),P.m(G,null))),r[3]?E?E.p(r,C):(E=be(r),E.c(),E.m(j,x)):E&&(E.d(1),E=null),Y===(Y=ue(r))&&I?I.p(r,C):(I.d(1),I=Y(r),I&&(I.c(),I.m(K,null)))},i:B,o:B,d(r){r&&L(e),le(y,r),le(w,r),A&&A.d(),P.d(),E&&E.d(),I.d()}}}function Ze(t,e,l){let n=[],s=[],i="",o="",p="",m=null;Ie(async()=>{try{l(2,i="verstak-gui"),window.go&&window.go.main&&window.go.main.App&&(l(2,i=await window.go.main.App.VerstakVersion()),l(0,n=await window.go.main.App.ListSections()),l(1,s=await window.go.main.App.ListRootNodes()))}catch(h){l(3,o=String(h))}});function u(h){l(4,p=h)}function _(h){l(5,m=h)}return[n,s,i,o,p,m,u,_,h=>u(h.id),h=>_(h)]}class xe extends De{constructor(e){super(),ze(this,e,Ze,Ye,Oe,{})}}new xe({target:document.getElementById("app")}); diff --git a/cmd/verstak-gui/frontend-dist/assets/index-ClxkTvdE.css b/cmd/verstak-gui/frontend-dist/assets/index-ClxkTvdE.css new file mode 100644 index 0000000..bda640a --- /dev/null +++ b/cmd/verstak-gui/frontend-dist/assets/index-ClxkTvdE.css @@ -0,0 +1 @@ +.svelte-14ysusk.svelte-14ysusk,.svelte-14ysusk.svelte-14ysusk:before,.svelte-14ysusk.svelte-14ysusk:after{box-sizing:border-box;margin:0;padding:0}.app.svelte-14ysusk.svelte-14ysusk{display:flex;width:100vw;height:100vh;overflow:hidden;background:#13131f;color:#e4e4ef;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px}.sidebar.svelte-14ysusk.svelte-14ysusk{width:260px;min-width:200px;height:100vh;display:flex;flex-direction:column;background:#1a1a28;border-right:1px solid #2a2a3c;flex-shrink:0;overflow:hidden}.sidebar-top.svelte-14ysusk.svelte-14ysusk{padding:16px 20px;display:flex;align-items:center;gap:10px;border-bottom:1px solid #2a2a3c;flex-shrink:0}.logo.svelte-14ysusk.svelte-14ysusk{font-size:20px;line-height:1}.app-name.svelte-14ysusk.svelte-14ysusk{font-size:16px;font-weight:600;color:#e4e4ef}.sidebar-nav.svelte-14ysusk.svelte-14ysusk{flex:1;overflow-y:auto;padding:12px 0}.nav-group.svelte-14ysusk.svelte-14ysusk{margin-bottom:16px}.nav-label.svelte-14ysusk.svelte-14ysusk{font-size:10px;text-transform:uppercase;letter-spacing:.5px;color:#666;padding:4px 20px;margin-bottom:4px}.nav-item.svelte-14ysusk.svelte-14ysusk{display:block;width:100%;padding:8px 20px;border:none;background:none;color:#ccc;font-size:13px;text-align:left;cursor:pointer;border-radius:0;font-family:inherit}.nav-item.svelte-14ysusk.svelte-14ysusk:hover{background:#223}.nav-item.selected.svelte-14ysusk.svelte-14ysusk{background:#2a2a4a;color:#fff;font-weight:500}.nav-empty.svelte-14ysusk.svelte-14ysusk{padding:8px 20px;color:#555;font-size:12px}.sidebar-bottom.svelte-14ysusk.svelte-14ysusk{padding:12px 20px;border-top:1px solid #2a2a3c;flex-shrink:0}.version.svelte-14ysusk.svelte-14ysusk{font-size:11px;color:#555}.main.svelte-14ysusk.svelte-14ysusk{flex:1;display:flex;flex-direction:column;height:100vh;min-width:0;overflow:hidden;background:#13131f}.header.svelte-14ysusk.svelte-14ysusk{padding:12px 24px;border-bottom:1px solid #2a2a3c;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;min-height:48px}.crumb.svelte-14ysusk.svelte-14ysusk{font-size:14px;font-weight:500;color:#e4e4ef}.crumb.placeholder.svelte-14ysusk.svelte-14ysusk{color:#666}.search-hint.svelte-14ysusk.svelte-14ysusk{padding:6px 12px;background:#1e1e2e;border:1px solid #2a2a3c;border-radius:4px;color:#666;font-size:12px;cursor:text}.error-banner.svelte-14ysusk.svelte-14ysusk{background:#3a2222;color:#f88;padding:8px 24px;font-size:12px;border-bottom:1px solid #4a2222;flex-shrink:0}.content.svelte-14ysusk.svelte-14ysusk{flex:1;overflow-y:auto;padding:24px}.welcome.svelte-14ysusk h2.svelte-14ysusk{font-size:28px;font-weight:300;margin-bottom:12px;color:#8888a4}.welcome.svelte-14ysusk p.svelte-14ysusk{color:#666;font-size:13px;margin-bottom:4px}.error-text.svelte-14ysusk.svelte-14ysusk{color:#f88;margin-top:12px}.loading.svelte-14ysusk.svelte-14ysusk{color:#666}.node-view.svelte-14ysusk h2.svelte-14ysusk{font-size:24px;margin-bottom:16px}.node-meta.svelte-14ysusk.svelte-14ysusk{display:flex;gap:16px;color:#666;font-size:12px} diff --git a/cmd/verstak-gui/main.go b/cmd/verstak-gui/main.go new file mode 100644 index 0000000..c127650 --- /dev/null +++ b/cmd/verstak-gui/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "embed" + "log" + "os" + "path/filepath" + + "verstak/internal/core/actions" + "verstak/internal/core/files" + "verstak/internal/core/notes" + "verstak/internal/core/nodes" + "verstak/internal/core/plugins" + "verstak/internal/core/search" + "verstak/internal/core/storage" + "verstak/internal/core/worklog" + + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" +) + +//go:embed all:frontend-dist +var assets embed.FS + +func main() { + vaultPath := "." + if len(os.Args) > 1 { + vaultPath = os.Args[1] + } + + abs, err := filepath.Abs(vaultPath) + if err != nil { + log.Fatal(err) + } + + dbPath := filepath.Join(abs, ".verstak", "index.db") + db, err := storage.Open(dbPath) + if err != nil { + log.Fatalf("Open vault: %v", err) + } + defer db.Close() + + // Init core services + nodeRepo := nodes.NewRepository(db) + fileSvc := files.NewService(db, abs) + noteSvc := notes.NewService(db, abs, nodeRepo, fileSvc) + actionSvc := actions.NewService(db) + worklogSvc := worklog.NewService(db) + searchSvc := search.NewService(db) + plugins.NewManager(abs).Discover() + _ = searchSvc + + app := &App{ + db: db, + nodes: nodeRepo, + files: fileSvc, + notes: noteSvc, + actions: actionSvc, + worklog: worklogSvc, + vault: abs, + } + + err = wails.Run(&options.App{ + Title: "Верстак", + Width: 1280, + Height: 800, + MinWidth: 800, + MinHeight: 600, + BackgroundColour: &options.RGBA{R: 19, G: 19, B: 31, A: 1}, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + OnStartup: app.startup, + Bind: []interface{}{app}, + }) + + if err != nil { + log.Fatal(err) + } +} diff --git a/frontend/src/api/verstak.js b/frontend/src/api/verstak.js new file mode 100644 index 0000000..0f713c4 --- /dev/null +++ b/frontend/src/api/verstak.js @@ -0,0 +1,43 @@ +// Wails v2 API wrapper — single frontend access point to Go backend + +function wailsCall(method, ...args) { + if (window.go && window.go.main && window.go.main.App) { + return window.go.main.App[method](...args) + } + return Promise.reject(new Error('Wails bindings not loaded')) +} + +// Sections +export const listSections = () => wailsCall('ListSections') + +// Nodes +export const listNodesBySection = (section) => wailsCall('ListNodesBySection', section) +export const listChildren = (parentID) => wailsCall('ListChildren', parentID) +export const getNodeDetail = (id) => wailsCall('GetNodeDetail', id) +export const createNode = (parentID, type, title, section) => + wailsCall('CreateNode', parentID, type, title, section) +export const deleteNode = (id) => wailsCall('DeleteNode', id) + +// Notes +export const listNotes = (nodeID) => wailsCall('ListNotes', nodeID) +export const createNote = (parentID, title) => wailsCall('CreateNote', parentID, title) +export const readNote = (noteID) => wailsCall('ReadNote', noteID) +export const saveNote = (noteID, content) => wailsCall('SaveNote', noteID, content) + +// Files +export const listFiles = (nodeID) => wailsCall('ListFiles', nodeID) + +// Actions +export const listActions = (nodeID) => wailsCall('ListActions', nodeID) +export const runAction = (id) => wailsCall('RunAction', id) + +// Worklog +export const listWorklog = (nodeID) => wailsCall('ListWorklog', nodeID) +export const createWorklog = (nodeID, summary, minutes) => + wailsCall('CreateWorklog', nodeID, summary, minutes) + +// Search +export const search = (query) => wailsCall('Search', query) + +// System +export const verstakVersion = () => wailsCall('VerstakVersion')