step 10: plugins system (Lua + templates) + DokuWiki as optional plugin
Plugin Manager: - Discover plugins from .verstak/plugins/<name>/plugin.json - Enable/disable per plugin - Template definitions (JSON) → pre-filled node trees - SQL migrations from plugins - Built-in templates loaded from internal/core/plugins/builtin/templates/ Lua Runtime: - Stub (gopher-lua placeholder) — ready for real implementation - When dep added: hooks (on_init, on_vault_open, on_node_create), sandbox (no io/os.execute), Plugin API GUI: - Template selector in create node modal - POST /api/nodes/from-template creates tree from template - Built-in "Клиент" template: Overview note + Документы/Переписка/Скриншоты CLI: - verstak plugin list/enable/disable/templates DokuWiki Importer: - Moved to contrib/plugins/importer-dokuwiki/ (optional plugin) - plugin.json + migration + README DokuWiki removed from MVP core — now an opt-in plugin. Acceptance: go build ./... pass, go test ./... pass (all packages).
This commit is contained in:
@@ -207,6 +207,8 @@ input[type=checkbox]{width:auto!important;margin-right:6px;display:inline}
|
||||
<select id="mn-type"><option value="case">◇ Дело</option><option value="folder">▸ Папка</option><option value="space">◎ Пространство</option><option value="recipe">◈ Рецепт</option></select>
|
||||
<label for="mn-title">Название</label><input id="mn-title" placeholder="Название...">
|
||||
<label for="mn-parent">Родитель (опционально)</label><input id="mn-parent" placeholder="Имя папки или оставьте пустым">
|
||||
<label>Шаблон</label>
|
||||
<select id="mn-tmpl"><option value="">— без шаблона —</option></select>
|
||||
<div class="ma"><button class="btn" onclick="closeM('m-node')">Отмена</button><button class="btn primary" onclick="submitNode()">Создать</button></div></div>
|
||||
</div>
|
||||
<div class="mo" id="m-note">
|
||||
@@ -674,7 +676,23 @@ async function submitWLEntry(nodeId){
|
||||
/* ════════════════════════════════════════════
|
||||
MODALS
|
||||
════════════════════════════════════════════ */
|
||||
function openM(id){G(id).classList.add('on');setTimeout(()=>{const i=G(id).querySelector('input,textarea');if(i)i.focus()},60)}
|
||||
function openM(id){
|
||||
G(id).classList.add('on');
|
||||
setTimeout(()=>{const i=G(id).querySelector('input,textarea');if(i)i.focus()},60);
|
||||
if(id==='m-node')loadTemplates();
|
||||
}
|
||||
async function loadTemplates(){
|
||||
const sel=document.getElementById('mn-tmpl');
|
||||
if(!sel)return;
|
||||
try{
|
||||
const tmpls=await api('/api/templates');
|
||||
let h='<option value="">— без шаблона —</option>';
|
||||
for(const t of tmpls){
|
||||
h+='<option value="'+esc(t.name)+'">'+esc(t.name)+' ['+esc(t.plugin)+']</option>';
|
||||
}
|
||||
sel.innerHTML=h;
|
||||
}catch(e){}
|
||||
}
|
||||
function closeM(id){G(id).classList.remove('on');G(id).querySelectorAll('input,textarea').forEach(e=>e.value='')}
|
||||
function doAdd(kind){
|
||||
closeAddMenu();
|
||||
@@ -720,6 +738,7 @@ document.addEventListener('click',()=>closeAddMenu());
|
||||
|
||||
async function submitNode(){
|
||||
const t=G('mn-type').value,title=G('mn-title').value.trim(),parentName=G('mn-parent').value.trim();
|
||||
const tmpl=G('mn-tmpl').value;
|
||||
if(!title)return;
|
||||
let parentId='', section='';
|
||||
if(parentName){
|
||||
@@ -732,7 +751,12 @@ async function submitNode(){
|
||||
section=sel.section;
|
||||
}
|
||||
try{
|
||||
const n=await api('/api/nodes',{method:'POST',body:JSON.stringify({parent_id:parentId,type:t,title,section})});
|
||||
let n;
|
||||
if(tmpl){
|
||||
n=await api('/api/nodes/from-template',{method:'POST',body:JSON.stringify({parent_id:parentId,type:t,title,section,template:tmpl})});
|
||||
}else{
|
||||
n=await api('/api/nodes',{method:'POST',body:JSON.stringify({parent_id:parentId,type:t,title,section})});
|
||||
}
|
||||
closeM('m-node');
|
||||
const qs = section||'';
|
||||
const items=await api('/api/nodes?section='+encodeURIComponent(qs));
|
||||
|
||||
+75
-1
@@ -13,6 +13,7 @@ import (
|
||||
"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"
|
||||
@@ -28,6 +29,7 @@ type Server struct {
|
||||
actions *actions.Service
|
||||
worklog *worklog.Service
|
||||
search *search.Service
|
||||
plugins *plugins.Manager
|
||||
srv *http.Server
|
||||
listener net.Listener
|
||||
port int
|
||||
@@ -41,10 +43,12 @@ func NewServer(db *storage.DB, vaultRoot string) *Server {
|
||||
actionSvc := actions.NewService(db)
|
||||
workSvc := worklog.NewService(db)
|
||||
srchSvc := search.NewService(db)
|
||||
pluginMgr := plugins.NewManager(vaultRoot)
|
||||
pluginMgr.Discover()
|
||||
return &Server{
|
||||
db: db, vaultRoot: vaultRoot,
|
||||
nodes: nodeRepo, files: fileSvc, notes: noteSvc, actions: actionSvc,
|
||||
worklog: workSvc, search: srchSvc,
|
||||
worklog: workSvc, search: srchSvc, plugins: pluginMgr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,11 +56,13 @@ func NewServer(db *storage.DB, vaultRoot string) *Server {
|
||||
func (s *Server) Start() (string, error) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/nodes", s.handleNodes)
|
||||
mux.HandleFunc("/api/nodes/from-template", s.handleNodeFromTemplate)
|
||||
mux.HandleFunc("/api/nodes/", s.handleNodeDetail)
|
||||
mux.HandleFunc("/api/notes/", s.handleNotes)
|
||||
mux.HandleFunc("/api/files/", s.handleFiles)
|
||||
mux.HandleFunc("/api/actions/", s.handleActions)
|
||||
mux.HandleFunc("/api/worklog/", s.handleWorklog)
|
||||
mux.HandleFunc("/api/templates", s.handleTemplates)
|
||||
mux.HandleFunc("/api/search", s.handleSearch)
|
||||
mux.HandleFunc("/", s.handleStatic)
|
||||
|
||||
@@ -151,6 +157,65 @@ func (s *Server) handleNodes(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/nodes/from-template — create node tree from a template.
|
||||
func (s *Server) handleNodeFromTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
jsonErr(w, 405, "method not allowed")
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
ParentID string `json:"parent_id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Section string `json:"section"`
|
||||
Template string `json:"template"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
// Find the template.
|
||||
var tmpl *plugins.TemplateDefinition
|
||||
for _, t := range s.plugins.Templates() {
|
||||
if t.Name == req.Template {
|
||||
tmpl = &t
|
||||
break
|
||||
}
|
||||
}
|
||||
if tmpl == nil {
|
||||
jsonErr(w, 404, "template not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Create root node.
|
||||
root, err := s.nodes.Create(req.ParentID, tmpl.RootType, req.Title, req.Section)
|
||||
if err != nil {
|
||||
jsonErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Create children recursively.
|
||||
var createTree func(parentID string, nodes []plugins.TreeNode) error
|
||||
createTree = func(parentID string, nodes []plugins.TreeNode) error {
|
||||
for _, tn := range nodes {
|
||||
child, err := s.nodes.Create(parentID, tn.Type, tn.Title, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tn.Children) > 0 {
|
||||
if err := createTree(child.ID, tn.Children); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := createTree(root.ID, tmpl.Tree); err != nil {
|
||||
jsonErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
jsonOK(w, root)
|
||||
}
|
||||
|
||||
// GET/PUT/DELETE /api/nodes/{id}
|
||||
func (s *Server) handleNodeDetail(w http.ResponseWriter, r *http.Request) {
|
||||
id := strings.TrimPrefix(r.URL.Path, "/api/nodes/")
|
||||
@@ -386,3 +451,12 @@ func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
jsonOK(w, results)
|
||||
}
|
||||
|
||||
// GET /api/templates — list all templates from active plugins.
|
||||
func (s *Server) handleTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
jsonErr(w, 405, "method not allowed")
|
||||
return
|
||||
}
|
||||
jsonOK(w, s.plugins.Templates())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user