Compare commits
No commits in common. "edc708a106ce9a8831c404d82f60ede9df2fc4f1" and "a098cf721c55c0353aab2a07534c6edc37eee388" have entirely different histories.
edc708a106
...
a098cf721c
|
|
@ -27,7 +27,6 @@ frontend/bindings/
|
||||||
|
|
||||||
# Vault data
|
# Vault data
|
||||||
.verstak/
|
.verstak/
|
||||||
spaces/
|
|
||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"verstak/internal/core/files"
|
"verstak/internal/core/files"
|
||||||
"verstak/internal/core/notes"
|
"verstak/internal/core/notes"
|
||||||
"verstak/internal/core/nodes"
|
"verstak/internal/core/nodes"
|
||||||
"verstak/internal/core/plugins"
|
|
||||||
"verstak/internal/core/search"
|
"verstak/internal/core/search"
|
||||||
"verstak/internal/core/storage"
|
"verstak/internal/core/storage"
|
||||||
"verstak/internal/core/worklog"
|
"verstak/internal/core/worklog"
|
||||||
|
|
@ -36,7 +35,6 @@ type App struct {
|
||||||
actions *actions.Service
|
actions *actions.Service
|
||||||
worklog *worklog.Service
|
worklog *worklog.Service
|
||||||
search *search.Service
|
search *search.Service
|
||||||
plugins *plugins.Manager
|
|
||||||
vault string
|
vault string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -394,66 +392,6 @@ func (a *App) DeleteNode(id string) error {
|
||||||
return a.nodes.SoftDelete(id)
|
return a.nodes.SoftDelete(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Templates
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
type TemplateDTO struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) ListTemplates() []TemplateDTO {
|
|
||||||
templates := a.plugins.Templates()
|
|
||||||
out := make([]TemplateDTO, 0, len(templates))
|
|
||||||
for _, t := range templates {
|
|
||||||
out = append(out, TemplateDTO{
|
|
||||||
Name: t.Name,
|
|
||||||
Description: t.Description,
|
|
||||||
Icon: t.Icon,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) FromTemplate(parentID, nodeType, title, section, template string) (*NodeDTO, error) {
|
|
||||||
var tmpl *plugins.TemplateDefinition
|
|
||||||
for _, t := range a.plugins.Templates() {
|
|
||||||
if t.Name == template {
|
|
||||||
tmpl = &t
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if tmpl == nil {
|
|
||||||
return nil, fmt.Errorf("template %q not found", template)
|
|
||||||
}
|
|
||||||
root, err := a.nodes.Create(parentID, tmpl.RootType, title, section)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var createTree func(parentID string, nodes []plugins.TreeNode) error
|
|
||||||
createTree = func(parentID string, nodes []plugins.TreeNode) error {
|
|
||||||
for _, tn := range nodes {
|
|
||||||
child, err := a.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 {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dto := toNodeDTO(root)
|
|
||||||
return &dto, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Notes
|
// Notes
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -720,24 +658,6 @@ func (a *App) ListActions(nodeID string) ([]ActionDTO, error) {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) CreateAction(nodeID, kind, title, data string) (*ActionDTO, error) {
|
|
||||||
rec, err := a.actions.Create(nodeID, kind, title, data, "", data, nil, kind == "run_command" || kind == "run_script", false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ActionDTO{
|
|
||||||
ID: rec.ID,
|
|
||||||
NodeID: rec.NodeID,
|
|
||||||
Title: rec.Title,
|
|
||||||
Type: rec.Kind,
|
|
||||||
Data: data,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) DeleteAction(id string) error {
|
|
||||||
return a.actions.Delete(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) RunAction(id string) error {
|
func (a *App) RunAction(id string) error {
|
||||||
_, err := a.actions.Run(id)
|
_, err := a.actions.Run(id)
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
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-DZkGJWBF.js"></script>
|
<script type="module" crossorigin src="/assets/main-DtITCkHU.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-BnVt-oqm.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-5x3eoU2l.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,7 @@ func main() {
|
||||||
activitySvc := activity.NewService(db)
|
activitySvc := activity.NewService(db)
|
||||||
worklogSvc := worklog.NewService(db)
|
worklogSvc := worklog.NewService(db)
|
||||||
searchSvc := search.NewService(db)
|
searchSvc := search.NewService(db)
|
||||||
pm := plugins.NewManager(abs)
|
plugins.NewManager(abs).Discover()
|
||||||
pm.Discover()
|
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
db: db,
|
db: db,
|
||||||
|
|
@ -62,7 +61,6 @@ func main() {
|
||||||
actions: actionSvc,
|
actions: actionSvc,
|
||||||
worklog: worklogSvc,
|
worklog: worklogSvc,
|
||||||
search: searchSvc,
|
search: searchSvc,
|
||||||
plugins: pm,
|
|
||||||
vault: abs,
|
vault: abs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@
|
||||||
|
|
||||||
```text
|
```text
|
||||||
┌────────────────────┐
|
┌────────────────────┐
|
||||||
│ GUI/Wails v2 │
|
│ GUI/Wails │
|
||||||
└─────────┬──────────┘
|
└─────────┬──────────┘
|
||||||
│
|
│
|
||||||
┌───────▼────────┐ ┌─────────────┐
|
┌─────────────┐ ┌───────▼────────┐ ┌─────────────┐
|
||||||
│ Core Library │◀───│ CLI Commands │
|
│ TUI/Bubble │───▶│ Core Library │◀───│ CLI Commands │
|
||||||
└───────┬────────┘ └─────────────┘
|
└─────────────┘ └───────┬────────┘ └─────────────┘
|
||||||
│
|
│
|
||||||
┌─────────▼──────────┐
|
┌─────────▼──────────┐
|
||||||
│ Local Vault+SQLite │
|
│ Local Vault+SQLite │
|
||||||
|
|
@ -161,6 +161,8 @@ verstak/
|
||||||
activity/
|
activity/
|
||||||
search/
|
search/
|
||||||
importers/
|
importers/
|
||||||
|
sync/
|
||||||
|
security/
|
||||||
config/
|
config/
|
||||||
|
|
||||||
gui/
|
gui/
|
||||||
|
|
|
||||||
|
|
@ -18,23 +18,23 @@
|
||||||
- import/dokuwiki/mysql-cleanup.txt
|
- import/dokuwiki/mysql-cleanup.txt
|
||||||
|
|
||||||
Быстрые действия:
|
Быстрые действия:
|
||||||
[Новое дело] [Быстрая заметка] [Добавить файл] [Импорт DokuWiki] [Sync] *(future)*
|
[Новое дело] [Быстрая заметка] [Добавить файл] [Импорт DokuWiki] [Sync]
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Общий layout
|
## 2. Общий layout
|
||||||
|
|
||||||
```text
|
```text
|
||||||
┌───────────────┬────────────────────────────────────┐
|
┌────────────────────────────────────────────────────┐
|
||||||
│ Tree sidebar │ Header: Section/Node Title │
|
│ Search | Quick Add | Sync Status | Settings │
|
||||||
│ ├────────────────────────────────────┤
|
├───────────────┬────────────────────────────────────┤
|
||||||
│ Сегодня │ Main content │
|
│ Tree sidebar │ Main content │
|
||||||
│ Клиенты │ Case / Note / File / Search │
|
│ │ │
|
||||||
|
│ Сегодня │ Case / Note / File / Search │
|
||||||
|
│ Клиенты │ │
|
||||||
│ Проекты │ │
|
│ Проекты │ │
|
||||||
│ Рецепты │ │
|
│ Рецепты │ │
|
||||||
│ Документы │ │
|
│ Документы │ │
|
||||||
└───────────────┴────────────────────────────────────┘
|
└───────────────┴────────────────────────────────────┘
|
||||||
|
|
||||||
*Примечание: Quick Add и Sync Status из оригинального макета пока не реализованы — в планах на будущее.*
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Sidebar tree
|
## 3. Sidebar tree
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
# Верстак — roadmap
|
# Верстак — roadmap
|
||||||
|
|
||||||
> **Wails v3 → v2:** The GUI was initially built with Wails v3 but repeatedly hit SIGSEGV on Linux during window close. Downgraded to Wails v2 which is stable. Milestone 4 below reflects the current (desktop) state.
|
|
||||||
|
|
||||||
## Milestone 0 — Skeleton
|
## Milestone 0 — Skeleton
|
||||||
|
|
||||||
- Go module;
|
- Go module;
|
||||||
|
|
@ -67,8 +65,6 @@ Acceptance:
|
||||||
|
|
||||||
## Milestone 4 — GUI MVP
|
## Milestone 4 — GUI MVP
|
||||||
|
|
||||||
> **Current state:** the GUI is a Wails v2 desktop app. Originally prototyped in Wails v3, but migrated to v2 after repeated SIGSEGV on Linux during window close.
|
|
||||||
|
|
||||||
- Wails app;
|
- Wails app;
|
||||||
- sidebar tree;
|
- sidebar tree;
|
||||||
- create node/note;
|
- create node/note;
|
||||||
|
|
@ -144,7 +140,7 @@ Acceptance:
|
||||||
|
|
||||||
- можно постепенно разобрать папку `work`.
|
- можно постепенно разобрать папку `work`.
|
||||||
|
|
||||||
## Milestone 10 — Sync server MVP (PAUSED)
|
## Milestone 10 — Sync server MVP
|
||||||
|
|
||||||
- HTTP server;
|
- HTTP server;
|
||||||
- API key;
|
- API key;
|
||||||
|
|
@ -170,7 +166,7 @@ Acceptance:
|
||||||
- конфликт не теряет данные;
|
- конфликт не теряет данные;
|
||||||
- можно восстановить vault на новой машине.
|
- можно восстановить vault на новой машине.
|
||||||
|
|
||||||
## Milestone 12 — Activity MVP (PAUSED)
|
## Milestone 12 — Activity MVP
|
||||||
|
|
||||||
- activity events;
|
- activity events;
|
||||||
- opened nodes;
|
- opened nodes;
|
||||||
|
|
@ -183,7 +179,7 @@ Acceptance:
|
||||||
|
|
||||||
- Верстак предлагает worklog на основе следов.
|
- Верстак предлагает worklog на основе следов.
|
||||||
|
|
||||||
## Milestone 13 — File scanner/watcher (PAUSED)
|
## Milestone 13 — File scanner/watcher
|
||||||
|
|
||||||
- snapshot scanner;
|
- snapshot scanner;
|
||||||
- fsnotify watcher;
|
- fsnotify watcher;
|
||||||
|
|
@ -195,7 +191,7 @@ Acceptance:
|
||||||
|
|
||||||
- изменения файлов снаружи приложения обнаруживаются.
|
- изменения файлов снаружи приложения обнаруживаются.
|
||||||
|
|
||||||
## Milestone 14 — TUI MVP (PAUSED)
|
## Milestone 14 — TUI MVP
|
||||||
|
|
||||||
- Bubble Tea;
|
- Bubble Tea;
|
||||||
- tree/search;
|
- tree/search;
|
||||||
|
|
@ -207,7 +203,7 @@ Acceptance:
|
||||||
|
|
||||||
- можно из терминала быстро работать с Верстаком.
|
- можно из терминала быстро работать с Верстаком.
|
||||||
|
|
||||||
## Milestone 15 — 1.0 polish (PAUSED)
|
## Milestone 15 — 1.0 polish
|
||||||
|
|
||||||
- installers;
|
- installers;
|
||||||
- backup export;
|
- backup export;
|
||||||
|
|
|
||||||
|
|
@ -6,19 +6,19 @@
|
||||||
|
|
||||||
## Обязательные функции
|
## Обязательные функции
|
||||||
|
|
||||||
- [x] Создать vault.
|
- [ ] Создать vault.
|
||||||
- [x] Создать дерево дел.
|
- [ ] Создать дерево дел.
|
||||||
- [x] Создать Markdown-заметку.
|
- [ ] Создать Markdown-заметку.
|
||||||
- [x] Редактировать Markdown-заметку.
|
- [ ] Редактировать Markdown-заметку.
|
||||||
- [x] Добавить файл в дело.
|
- [ ] Добавить файл в дело.
|
||||||
- [x] Открыть файл системным приложением.
|
- [ ] Открыть файл системным приложением.
|
||||||
- [x] Создать action “Открыть URL”.
|
- [ ] Создать action “Открыть URL”.
|
||||||
- [x] Создать action “Открыть папку”.
|
- [ ] Создать action “Открыть папку”.
|
||||||
- [x] Создать action “Запустить команду”.
|
- [ ] Создать action “Запустить команду”.
|
||||||
- [x] Добавить запись работы.
|
- [ ] Добавить запись работы.
|
||||||
- [x] Скопировать отчёт по работам.
|
- [ ] Скопировать отчёт по работам.
|
||||||
- [x] Поиск по заметкам.
|
- [ ] Поиск по заметкам.
|
||||||
- [x] Поиск по именам файлов.
|
- [ ] Поиск по именам файлов.
|
||||||
- [x] Поиск по журналу работ.
|
- [x] Поиск по журналу работ.
|
||||||
- [x] Базовый импорт DokuWiki (плагин).
|
- [x] Базовый импорт DokuWiki (плагин).
|
||||||
|
|
||||||
|
|
|
||||||
155
docs/PLAN.md
155
docs/PLAN.md
|
|
@ -21,21 +21,21 @@
|
||||||
| 8 | FTS5 Search | ✅ выполнен |
|
| 8 | FTS5 Search | ✅ выполнен |
|
||||||
| 9 | Section assignment + Sidebar filtering | ✅ выполнен |
|
| 9 | Section assignment + Sidebar filtering | ✅ выполнен |
|
||||||
| 10 | Plugin Manager (discovery + templates) | ✅ выполнен |
|
| 10 | Plugin Manager (discovery + templates) | ✅ выполнен |
|
||||||
| 11 | **Wails Desktop GUI** | ✅ выполнено (v2, full Svelte UI) |
|
| 11 | **Wails Desktop GUI** | 🔄 Wails v2 vertical MVP |
|
||||||
| 12 | **Files/Folders full workflow** | ✅ выполнено (copy/link/import/tree) |
|
| 12 | **Files/Folders full workflow** | ⬜ следующий этап после vertical MVP |
|
||||||
| 13 | **Drag-and-drop** | ✅ выполнено (internal + external drops) |
|
| 13 | **Drag-and-drop** | ⬜ не начат |
|
||||||
| 14 | **MVP stabilization** | 🔄 в процессе — smoke-тесты, docs, go test |
|
| 14 | **MVP stabilization** | ⬜ не начат |
|
||||||
| 15 | Sync Server + Client | 🔒 PAUSED — HTTP API key, push/pull, blob sync |
|
| 15 | Sync Server Skeleton | 🔒 PAUSED |
|
||||||
| 16 | Activity Suggestions | 🔒 PAUSED — worklog suggestions from activity_events |
|
| 16 | Sync Client MVP | 🔒 PAUSED |
|
||||||
| 17 | File Scanner/Watcher | 🔒 PAUSED — fsnotify, snapshot scanner, missing file detection |
|
| 17 | Activity + File Scanner/Watcher | 🔒 PAUSED |
|
||||||
| 18 | TUI MVP (Bubble Tea) | 🔒 PAUSED — tree/search, add worklog, run action, sync |
|
| 18 | TUI MVP (Bubble Tea) | 🔒 PAUSED |
|
||||||
| 19 | Plugins: Lua runtime | 🔒 PAUSED — gopher-lua, hooks, sandbox |
|
| 19 | Integrity Check + Repair | 🔒 PAUSED |
|
||||||
| 20 | Browser Extension | 🔒 PAUSED — tracking, capture, evidence |
|
| 20 | Plugins: Lua runtime | 🔒 PAUSED |
|
||||||
| 21 | Calendar/Kanban | 🔒 PAUSED — view by date, board view |
|
| 21 | DokuWiki Importer (plugin) | 🔒 PAUSED |
|
||||||
| 22 | Integrity Check + Repair | 🔒 PAUSED — checksums, crash recovery |
|
| 22 | Calendar/Kanban | 🔒 PAUSED |
|
||||||
| 23 | New templates/integrations | 🔒 PAUSED — community plugins |
|
| 23 | New templates/integrations | 🔒 PAUSED |
|
||||||
|
|
||||||
> 🔒 = **PAUSED** — не начинать до завершения шага 14 (MVP stabilization). Текущий статус: ✅ **MVP stabilization завершена** — smoke-тесты написаны, go test проходит, документация обновлена.
|
> 🔒 = **PAUSED** — не начинать до завершения шага 14 (MVP stabilization).
|
||||||
|
|
||||||
> **Wails v3 → v2 migration:** Wails v3 alpha.96 показал SIGSEGV на Linux desktop (GTK/X11). Wails v2 stable выбран как GUI base для MVP. Миграция в процессе (ветка `gui/migrate-wails-v2`).
|
> **Wails v3 → v2 migration:** Wails v3 alpha.96 показал SIGSEGV на Linux desktop (GTK/X11). Wails v2 stable выбран как GUI base для MVP. Миграция в процессе (ветка `gui/migrate-wails-v2`).
|
||||||
|
|
||||||
|
|
@ -47,37 +47,35 @@ go build -tags "gui production webkit2_41" -o verstak-gui ./cmd/verstak-gui
|
||||||
./verstak-gui
|
./verstak-gui
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**GUI Build (Wails v2):**
|
||||||
|
```bash
|
||||||
|
cd frontend && npm run build && cd ..
|
||||||
|
rm -rf cmd/verstak-gui/frontend-dist && cp -r frontend/dist cmd/verstak-gui/frontend-dist
|
||||||
|
go build -tags "gui production webkit2_41" -o verstak-gui ./cmd/verstak-gui
|
||||||
|
./verstak-gui
|
||||||
|
```
|
||||||
|
|
||||||
Или для dev режима: `wails dev` (требует Wails v2 CLI)
|
Или для dev режима: `wails dev` (требует Wails v2 CLI)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Текущий этап: MVP Stabilization
|
## Текущий этап: Wails v2 Vertical MVP
|
||||||
|
|
||||||
**Цель:** стабилизация MVP — smoke-тесты, go test, документация.
|
**Цель:** базовый рабочий desktop GUI для разделов → дел → заметок.
|
||||||
|
|
||||||
**Прогресс Wails v2 Desktop GUI:**
|
**Прогресс:**
|
||||||
- ✅ Wails v2 shell (window opens, no SIGSEGV)
|
- ✅ Wails v2 shell (window opens, no SIGSEGV)
|
||||||
- ✅ Layout fix (full viewport, dark theme, sidebar+main)
|
- ✅ Layout fix (full viewport, dark theme, sidebar+main)
|
||||||
- ✅ Notes bindings + UI (create/read/save/dirty state)
|
- 🔄 Notes bindings + UI
|
||||||
- ✅ Tabs (Overview/Notes/Files/Actions/Worklog/Activity)
|
- 🔄 Tabs (Overview/Notes/Files/Actions/Worklog/Activity)
|
||||||
- ✅ Node creation + template selection (FromTemplate)
|
- 🔄 Node creation
|
||||||
- ✅ Section filtering
|
- 🔄 Section filtering
|
||||||
- ✅ File tree with breadcrumbs, preview, CRUD (rename/delete/duplicate/cut/copy/paste)
|
|
||||||
- ✅ Drag-and-drop (internal + external OS file drops)
|
|
||||||
- ✅ Actions CRUD (create/list/run/delete)
|
|
||||||
- ✅ Worklog entry form
|
|
||||||
- ✅ Today dashboard + Activity feed (global + per-case)
|
|
||||||
- ✅ Search
|
|
||||||
- ✅ Import dialog with safety checks (PreviewImport)
|
|
||||||
- ✅ Keyboard shortcuts
|
|
||||||
|
|
||||||
**Среднесрочные шаги (заморожены до стабилизации MVP):**
|
**Пауза (не начинать до завершения vertical MVP):**
|
||||||
- Sync server/client
|
- Файлы/папки workflow
|
||||||
- File Scanner/Watcher
|
- Drag-and-drop
|
||||||
- TUI (Bubble Tea)
|
- Sync, plugins, Lua, browser extension, TUI
|
||||||
- Activity suggestions
|
- Новые шаблоны, DokuWiki importer
|
||||||
- Lua runtime
|
|
||||||
- Browser extension
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -177,65 +175,38 @@ go build -tags "gui production webkit2_41" -o verstak-gui ./cmd/verstak-gui
|
||||||
4. Перенести текущий UI shell из inline HTML в frontend
|
4. Перенести текущий UI shell из inline HTML в frontend
|
||||||
5. Оставить текущий `internal/gui/` как legacy (не удалять, но не развивать)
|
5. Оставить текущий `internal/gui/` как legacy (не удалять, но не развивать)
|
||||||
|
|
||||||
### Backend bindings (Wails v2 — реализовано)
|
### Backend bindings (минимум)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Nodes
|
// Nodes
|
||||||
ListSections() []SectionDTO
|
ListSections() ([]SectionDTO, error)
|
||||||
ListNodesBySection(section string) ([]NodeDTO, error)
|
ListNodes(section string) ([]NodeDTO, error)
|
||||||
GetNodeDetail(nodeID string) (*NodeDTO, error)
|
GetNodeDetail(nodeID string) (NodeDetailDTO, error)
|
||||||
ListChildren(parentID string) ([]NodeDTO, error)
|
CreateNode(parentID, section, typ, title string) (NodeDTO, error)
|
||||||
CreateNode(parentID, typ, title, section string) (*NodeDTO, error)
|
FromTemplate(parentID, section, typ, title, template string) (NodeDTO, error)
|
||||||
DeleteNode(id string) error
|
DeleteNode(id string) error
|
||||||
MoveNode(nodeID, newParentID string) error
|
MoveNode(id, parentID string) error
|
||||||
RenameNode(nodeID, newTitle string) error
|
|
||||||
|
|
||||||
// Templates
|
|
||||||
ListTemplates() []TemplateDTO
|
|
||||||
FromTemplate(parentID, typ, title, section, template string) (*NodeDTO, error)
|
|
||||||
|
|
||||||
// Notes
|
// Notes
|
||||||
ListNotes(nodeID string) ([]NodeDTO, error)
|
CreateNote(parentID, title string) (NodeDTO, error)
|
||||||
CreateNote(parentID, title string) (*NodeDTO, error)
|
|
||||||
ReadNote(noteID string) (string, error)
|
ReadNote(noteID string) (string, error)
|
||||||
SaveNote(noteID, content string) error
|
SaveNote(noteID, content string) error
|
||||||
|
|
||||||
// Files
|
// Files
|
||||||
ListFiles(nodeID string) ([]FileDTO, error)
|
ListFiles(nodeID string) ([]FileDTO, error)
|
||||||
ListItems(nodeID string) ([]FileTreeItemDTO, error)
|
AddPathCopy(nodeID, sourcePath string) ([]NodeDTO, error) // файл или папка
|
||||||
AddPathCopy(nodeID, sourcePath string) ([]NodeDTO, error)
|
|
||||||
AddPathLink(nodeID, sourcePath string) ([]NodeDTO, error)
|
AddPathLink(nodeID, sourcePath string) ([]NodeDTO, error)
|
||||||
DeleteFileOrFolder(nodeID string) error
|
DeleteFileOrFolder(id string) error
|
||||||
CreateEmptyFile(parentID, filename string) (*NodeDTO, error)
|
OpenFile(id string) error
|
||||||
DuplicateNode(nodeID string) (*NodeDTO, error)
|
OpenFolder(id string) error
|
||||||
OpenFile(fileID string) error
|
|
||||||
OpenFolder(nodeID string) error
|
|
||||||
ReadFileText(fileID string) (string, error)
|
|
||||||
GetFileBase64(fileID string) (string, error)
|
|
||||||
PreviewImport(sourcePath string) (*ImportSummary, error)
|
|
||||||
ValidateName(name string) error
|
|
||||||
PickFile() (string, error)
|
PickFile() (string, error)
|
||||||
PickFiles() ([]string, error)
|
|
||||||
PickDirectory() (string, error)
|
PickDirectory() (string, error)
|
||||||
|
|
||||||
// Actions
|
// Actions/Worklog/Search
|
||||||
ListActions(nodeID string) ([]ActionDTO, error)
|
ListActions(nodeID string) ([]ActionDTO, error)
|
||||||
CreateAction(nodeID, kind, title, data string) (*ActionDTO, error)
|
|
||||||
DeleteAction(id string) error
|
|
||||||
RunAction(id string) error
|
RunAction(id string) error
|
||||||
|
CreateWorklog(nodeID, summary string, minutes int) (WorklogDTO, error)
|
||||||
// Worklog
|
|
||||||
ListWorklog(nodeID string) ([]WorklogDTO, error)
|
|
||||||
CreateWorklog(nodeID, summary string, minutes int) (*WorklogDTO, error)
|
|
||||||
|
|
||||||
// Search
|
|
||||||
Search(query string) ([]SearchResultDTO, error)
|
Search(query string) ([]SearchResultDTO, error)
|
||||||
|
|
||||||
// Activity
|
|
||||||
ListTodayView() (*TodayDashboardDTO, error)
|
|
||||||
ListActivityFeed(limit, offset int) ([]EventDTO, error)
|
|
||||||
ListActivityByNode(nodeID string, limit, offset int) ([]EventDTO, error)
|
|
||||||
CountActivityByNode(nodeID string) (int, error)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -340,22 +311,18 @@ verstak/
|
||||||
|
|
||||||
cmd/
|
cmd/
|
||||||
verstak/ # CLI
|
verstak/ # CLI
|
||||||
verstak-gui/ # Wails v2 GUI main
|
verstak-gui/ # Wails GUI main
|
||||||
|
verstak-tui/ # Bubble Tea TUI
|
||||||
|
verstak-server/ # Sync server
|
||||||
|
|
||||||
frontend/ # Wails v2 Svelte frontend
|
frontend/ # Wails frontend
|
||||||
package.json
|
package.json
|
||||||
vite.config.js
|
wails.json
|
||||||
src/
|
src/
|
||||||
App.svelte
|
App.svelte
|
||||||
FileTreeRow.svelte
|
components/
|
||||||
lib/
|
stores/
|
||||||
FileBreadcrumbs.svelte
|
styles/
|
||||||
FilePreviewModal.svelte
|
|
||||||
ConfirmModal.svelte
|
|
||||||
FileIcon.svelte
|
|
||||||
FileActions.svelte
|
|
||||||
fileUtils.js
|
|
||||||
api/verstak.js
|
|
||||||
|
|
||||||
internal/
|
internal/
|
||||||
core/
|
core/
|
||||||
|
|
@ -368,8 +335,10 @@ verstak/
|
||||||
worklog/
|
worklog/
|
||||||
activity/
|
activity/
|
||||||
search/
|
search/
|
||||||
|
sync/
|
||||||
|
security/
|
||||||
config/
|
config/
|
||||||
plugins/ # manager, lua (stub), builtin templates
|
plugins/ # manager, lua (stub)
|
||||||
|
|
||||||
contrib/
|
contrib/
|
||||||
plugins/
|
plugins/
|
||||||
|
|
@ -382,13 +351,11 @@ verstak/
|
||||||
004_add_notes.sql
|
004_add_notes.sql
|
||||||
005_add_actions.sql
|
005_add_actions.sql
|
||||||
006_add_worklog.sql
|
006_add_worklog.sql
|
||||||
007_search.sql
|
|
||||||
008_sync.sql
|
|
||||||
009_section.sql
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## RAID
|
## RAID
|
||||||
|
|
||||||
- **Критично:** Wails требует Node.js для frontend-сборки
|
- **Критично:** Wails требует Node.js для frontend-сборки
|
||||||
- **Критично:** go-sqlite3 + cgo; gcc уже установлен
|
- **Критично:** go-sqlite3 + cgo; gcc уже установлен
|
||||||
- **Зависимость:** Steps 15+ ждут завершения step 14 (MVP stabilization)
|
- **Зависимость:** Steps 15+ ждут завершения step 14
|
||||||
|
- **Риск:** Wails v3 может быть нестабилен — проверить перед шагом 11
|
||||||
|
|
|
||||||
|
|
@ -47,23 +47,8 @@
|
||||||
let showCreateNode = false
|
let showCreateNode = false
|
||||||
let newNodeTitle = ''
|
let newNodeTitle = ''
|
||||||
let newNodeSection = 'clients'
|
let newNodeSection = 'clients'
|
||||||
let newNodeTemplate = ''
|
|
||||||
let templates = []
|
|
||||||
let showCreateNote = false
|
let showCreateNote = false
|
||||||
let newNoteTitle = ''
|
let newNoteTitle = ''
|
||||||
let showCreateAction = false
|
|
||||||
let newActionTitle = ''
|
|
||||||
let newActionKind = 'open_url'
|
|
||||||
let newActionData = ''
|
|
||||||
let actionKinds = [
|
|
||||||
{ id: 'open_url', label: 'Открыть URL' },
|
|
||||||
{ id: 'open_file', label: 'Открыть файл' },
|
|
||||||
{ id: 'open_folder', label: 'Открыть папку' },
|
|
||||||
{ id: 'run_command', label: 'Запустить команду' },
|
|
||||||
{ id: 'run_script', label: 'Запустить скрипт' },
|
|
||||||
{ id: 'open_terminal', label: 'Открыть терминал' },
|
|
||||||
{ id: 'launch_app', label: 'Запустить приложение' },
|
|
||||||
]
|
|
||||||
let loading = true
|
let loading = true
|
||||||
let importing = false
|
let importing = false
|
||||||
let importSummary = null
|
let importSummary = null
|
||||||
|
|
@ -601,22 +586,14 @@
|
||||||
showCreateNode = true
|
showCreateNode = true
|
||||||
newNodeTitle = ''
|
newNodeTitle = ''
|
||||||
newNodeSection = selectedSection || 'clients'
|
newNodeSection = selectedSection || 'clients'
|
||||||
newNodeTemplate = ''
|
|
||||||
wailsCall('ListTemplates').then(t => { templates = t || [] }).catch(() => { templates = [] })
|
|
||||||
}
|
}
|
||||||
function cancelCreateNode() { showCreateNode = false; newNodeTitle = '' }
|
function cancelCreateNode() { showCreateNode = false; newNodeTitle = '' }
|
||||||
async function submitCreateNode() {
|
async function submitCreateNode() {
|
||||||
if (!newNodeTitle.trim()) return
|
if (!newNodeTitle.trim()) return
|
||||||
try {
|
try {
|
||||||
let node
|
const node = await wailsCall('CreateNode', '', 'case', newNodeTitle.trim(), newNodeSection)
|
||||||
if (newNodeTemplate) {
|
|
||||||
node = await wailsCall('FromTemplate', '', 'case', newNodeTitle.trim(), newNodeSection, newNodeTemplate)
|
|
||||||
} else {
|
|
||||||
node = await wailsCall('CreateNode', '', 'case', newNodeTitle.trim(), newNodeSection)
|
|
||||||
}
|
|
||||||
showCreateNode = false
|
showCreateNode = false
|
||||||
newNodeTitle = ''
|
newNodeTitle = ''
|
||||||
newNodeTemplate = ''
|
|
||||||
await selectSection(newNodeSection)
|
await selectSection(newNodeSection)
|
||||||
} catch (e) { error = String(e) }
|
} catch (e) { error = String(e) }
|
||||||
}
|
}
|
||||||
|
|
@ -843,37 +820,6 @@
|
||||||
if (n >= 2 && n <= 4) return few
|
if (n >= 2 && n <= 4) return few
|
||||||
return many
|
return many
|
||||||
}
|
}
|
||||||
// ===== Actions =====
|
|
||||||
function openCreateAction() {
|
|
||||||
showCreateAction = true
|
|
||||||
newActionTitle = ''
|
|
||||||
newActionKind = 'open_url'
|
|
||||||
newActionData = ''
|
|
||||||
}
|
|
||||||
function cancelCreateAction() { showCreateAction = false; newActionTitle = ''; newActionData = '' }
|
|
||||||
async function submitCreateAction() {
|
|
||||||
if (!newActionTitle.trim() || !newActionData.trim() || !selectedNode) return
|
|
||||||
try {
|
|
||||||
const action = await wailsCall('CreateAction', selectedNode.id, newActionKind, newActionTitle.trim(), newActionData.trim())
|
|
||||||
if (action && action.id) {
|
|
||||||
actions = [...actions, action]
|
|
||||||
}
|
|
||||||
showCreateAction = false
|
|
||||||
newActionTitle = ''
|
|
||||||
newActionData = ''
|
|
||||||
} catch (e) { error = String(e) }
|
|
||||||
}
|
|
||||||
async function deleteAction(id) {
|
|
||||||
try {
|
|
||||||
await wailsCall('DeleteAction', id)
|
|
||||||
actions = actions.filter(a => a.id !== id)
|
|
||||||
} catch (e) { error = String(e) }
|
|
||||||
}
|
|
||||||
function actionKindLabel(kind) {
|
|
||||||
const k = actionKinds.find(k => k.id === kind)
|
|
||||||
return k ? k.label : kind
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openNodeById(id) {
|
async function openNodeById(id) {
|
||||||
try {
|
try {
|
||||||
const node = await wailsCall('GetNodeDetail', id)
|
const node = await wailsCall('GetNodeDetail', id)
|
||||||
|
|
@ -983,7 +929,7 @@
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
||||||
Добавить файл
|
Добавить файл
|
||||||
</button>
|
</button>
|
||||||
<button class="qa-btn" on:click={openCreateAction}>
|
<button class="qa-btn" disabled title="Следующий этап">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
|
||||||
Добавить действие
|
Добавить действие
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1135,30 +1081,16 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{:else if activeTab === 'actions'}
|
{:else if activeTab === 'actions'}
|
||||||
<div class="actions-tab">
|
{#if actions.length === 0}
|
||||||
<div class="tab-toolbar">
|
<div class="empty-state"><p>Действий пока нет</p></div>
|
||||||
<button class="btn btn-primary" on:click={openCreateAction}>+ Добавить действие</button>
|
{:else}
|
||||||
</div>
|
{#each actions as action}
|
||||||
{#if actions.length === 0}
|
<div class="action-card">
|
||||||
<div class="empty-state"><p>Действий пока нет</p></div>
|
<span>{action.title}</span><span class="action-type">{action.type}</span>
|
||||||
{:else}
|
<button class="btn btn-sm" on:click={() => wailsCall('RunAction', action.id)}>Запустить</button>
|
||||||
{#each actions as action}
|
</div>
|
||||||
<div class="action-card">
|
{/each}
|
||||||
<div class="action-info">
|
{/if}
|
||||||
<span class="action-title">{action.title}</span>
|
|
||||||
<span class="action-type">{actionKindLabel(action.type)}</span>
|
|
||||||
<span class="action-data">{action.data}</span>
|
|
||||||
</div>
|
|
||||||
<div class="action-btns">
|
|
||||||
<button class="btn btn-sm" on:click={() => wailsCall('RunAction', action.id)}>Запустить</button>
|
|
||||||
<button class="btn btn-sm btn-danger" on:click={() => deleteAction(action.id)}>
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{:else if activeTab === 'worklog'}
|
{:else if activeTab === 'worklog'}
|
||||||
<div class="worklog-tab">
|
<div class="worklog-tab">
|
||||||
|
|
@ -1321,17 +1253,6 @@
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{#if templates.length > 0}
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Шаблон (опционально)</label>
|
|
||||||
<select bind:value={newNodeTemplate}>
|
|
||||||
<option value="">Без шаблона</option>
|
|
||||||
{#each templates as t}
|
|
||||||
<option value={t.name}>{t.name}{t.description ? ' — ' + t.description : ''}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn btn-primary" on:click={submitCreateNode}>Создать</button>
|
<button class="btn btn-primary" on:click={submitCreateNode}>Создать</button>
|
||||||
<button class="btn" on:click={cancelCreateNode}>Отмена</button>
|
<button class="btn" on:click={cancelCreateNode}>Отмена</button>
|
||||||
|
|
@ -1340,37 +1261,6 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showCreateAction}
|
|
||||||
<div class="modal-overlay" on:click|self={cancelCreateAction}>
|
|
||||||
<div class="modal">
|
|
||||||
<h3>Новое действие</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Название</label>
|
|
||||||
<input type="text" placeholder="Например: Открыть сайт" bind:value={newActionTitle}
|
|
||||||
on:keydown={(e) => e.key === 'Enter' && submitCreateAction()} autofocus />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Тип</label>
|
|
||||||
<select bind:value={newActionKind}>
|
|
||||||
{#each actionKinds as k}
|
|
||||||
<option value={k.id}>{k.label}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>{newActionKind === 'open_url' ? 'URL' : newActionKind === 'open_folder' || newActionKind === 'open_file' ? 'Путь' : 'Команда'}</label>
|
|
||||||
<input type="text" placeholder={newActionKind === 'open_url' ? 'https://example.com' : newActionKind === 'open_folder' || newActionKind === 'open_file' ? '/path/to/file' : 'команда'}
|
|
||||||
bind:value={newActionData}
|
|
||||||
on:keydown={(e) => e.key === 'Enter' && submitCreateAction()} />
|
|
||||||
</div>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<button class="btn btn-primary" on:click={submitCreateAction}>Создать</button>
|
|
||||||
<button class="btn" on:click={cancelCreateAction}>Отмена</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if showImportDialog && importSummary}
|
{#if showImportDialog && importSummary}
|
||||||
<div class="modal-overlay" on:click|self={cancelImport}>
|
<div class="modal-overlay" on:click|self={cancelImport}>
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
|
|
@ -1515,15 +1405,8 @@
|
||||||
.wl-meta { font-size: 11px; color: #555; margin-top: 2px; }
|
.wl-meta { font-size: 11px; color: #555; margin-top: 2px; }
|
||||||
|
|
||||||
/* Actions */
|
/* Actions */
|
||||||
.actions-tab { padding: 24px; }
|
.action-card { background: #1a1a28; padding: 12px 16px; border-radius: 8px; display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
|
||||||
.action-card { background: #1a1a28; padding: 12px 16px; border-radius: 8px; display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 8px; }
|
.action-type { font-size: 11px; color: #888; background: #222233; padding: 2px 8px; border-radius: 10px; }
|
||||||
.action-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
|
|
||||||
.action-title { font-weight: 500; }
|
|
||||||
.action-type { font-size: 11px; color: #888; background: #222233; padding: 2px 8px; border-radius: 10px; white-space: nowrap; }
|
|
||||||
.action-data { font-size: 11px; color: #555; font-family: 'SF Mono', 'Fira Code', monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px; }
|
|
||||||
.action-btns { display: flex; gap: 4px; flex-shrink: 0; }
|
|
||||||
.action-btns .btn-danger { color: #ff6b6b; border-color: #4a2222; padding: 4px 8px; }
|
|
||||||
.action-btns .btn-danger:hover { background: #3a2222; }
|
|
||||||
|
|
||||||
/* Empty states */
|
/* Empty states */
|
||||||
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; }
|
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; }
|
||||||
|
|
|
||||||
|
|
@ -1,278 +0,0 @@
|
||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"verstak/internal/core/actions"
|
|
||||||
"verstak/internal/core/files"
|
|
||||||
"verstak/internal/core/notes"
|
|
||||||
"verstak/internal/core/nodes"
|
|
||||||
"verstak/internal/core/search"
|
|
||||||
"verstak/internal/core/storage"
|
|
||||||
"verstak/internal/core/vault"
|
|
||||||
"verstak/internal/core/worklog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func openTestDB(t *testing.T) *storage.DB {
|
|
||||||
t.Helper()
|
|
||||||
dir := t.TempDir()
|
|
||||||
db, err := storage.Open(filepath.Join(dir, "test.db"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("open db: %v", err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() { db.Close() })
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMVPSmoke(t *testing.T) {
|
|
||||||
// Create a test vault with a real filesystem area.
|
|
||||||
vaultDir := t.TempDir()
|
|
||||||
|
|
||||||
// 1. Init vault.
|
|
||||||
if err := vault.Init(vaultDir); err != nil {
|
|
||||||
t.Fatalf("vault init: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db := openTestDB(t)
|
|
||||||
nodeRepo := nodes.NewRepository(db)
|
|
||||||
fileSvc := files.NewService(db, vaultDir, nodeRepo)
|
|
||||||
noteSvc := notes.NewService(db, vaultDir, nodeRepo, fileSvc)
|
|
||||||
actionSvc := actions.NewService(db)
|
|
||||||
worklogSvc := worklog.NewService(db)
|
|
||||||
searchSvc := search.NewService(db)
|
|
||||||
|
|
||||||
// 2. Create client case structure.
|
|
||||||
client, err := nodeRepo.Create("", nodes.TypeCase, "ООО Ромашка", "clients")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create client: %v", err)
|
|
||||||
}
|
|
||||||
project, err := nodeRepo.Create(client.ID, nodes.TypeCase, "Сайт", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create project: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Create a Markdown note.
|
|
||||||
noteNode, noteRec, err := noteSvc.Create(project.ID, "overview.md", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create note: %v", err)
|
|
||||||
}
|
|
||||||
if noteNode.Title != "overview.md" {
|
|
||||||
t.Errorf("note title = %q, want overview.md", noteNode.Title)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Write note content.
|
|
||||||
content := "# Сайт ООО Ромашка\n\nWordPress/nginx. Иногда обновление."
|
|
||||||
if err := noteSvc.Save(noteNode.ID, content); err != nil {
|
|
||||||
t.Fatalf("save note: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Read note back.
|
|
||||||
got, err := noteSvc.Read(noteNode.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read note: %v", err)
|
|
||||||
}
|
|
||||||
if got != content {
|
|
||||||
t.Errorf("note content mismatch: got %q, want %q", got, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Verify note file exists on disk.
|
|
||||||
noteFileRecs, _ := fileSvc.ListByNode(noteNode.ID)
|
|
||||||
notePath := ""
|
|
||||||
if len(noteFileRecs) > 0 {
|
|
||||||
notePath = noteFileRecs[0].Path
|
|
||||||
if _, err := os.Stat(notePath); os.IsNotExist(err) {
|
|
||||||
t.Errorf("note file not on disk: %s", notePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = noteRec
|
|
||||||
|
|
||||||
// 7. Add a dummy external file.
|
|
||||||
dummyPath := filepath.Join(vaultDir, "dogovor.docx")
|
|
||||||
if err := os.WriteFile(dummyPath, []byte("dummy contract data"), 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
created, err := fileSvc.AddPathCopy(project.ID, dummyPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("add file: %v", err)
|
|
||||||
}
|
|
||||||
if len(created) == 0 {
|
|
||||||
t.Fatal("no nodes created from file import")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Open the imported file (just verify it opens without error).
|
|
||||||
fileNode := created[0]
|
|
||||||
if fileNode.Type != nodes.TypeFile {
|
|
||||||
t.Errorf("expected file type, got %s", fileNode.Type)
|
|
||||||
}
|
|
||||||
fileRecs, err := fileSvc.ListByNode(fileNode.ID)
|
|
||||||
if err != nil || len(fileRecs) == 0 {
|
|
||||||
t.Fatalf("file record not found for imported file")
|
|
||||||
}
|
|
||||||
_ = fileSvc.Open(fileRecs[0].ID) // may fail in test (no display)
|
|
||||||
|
|
||||||
// 9. Verify the imported file node exists as a child of the project.
|
|
||||||
children, err := nodeRepo.ListChildren(project.ID, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("list children: %v", err)
|
|
||||||
}
|
|
||||||
hasImported := false
|
|
||||||
for _, c := range children {
|
|
||||||
if c.Type == nodes.TypeFile && c.Title == "dogovor.docx" {
|
|
||||||
hasImported = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !hasImported {
|
|
||||||
t.Error("imported file node not found under project")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10. Create actions.
|
|
||||||
urlAction, err := actionSvc.Create(project.ID, actions.KindOpenURL, "Открыть сайт", "", "", "https://example.com", nil, false, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create url action: %v", err)
|
|
||||||
}
|
|
||||||
if urlAction.Kind != actions.KindOpenURL {
|
|
||||||
t.Errorf("action kind = %s, want %s", urlAction.Kind, actions.KindOpenURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
folderAction, err := actionSvc.Create(project.ID, actions.KindOpenFolder, "Открыть папку", vaultDir, "", "", nil, false, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create folder action: %v", err)
|
|
||||||
}
|
|
||||||
if folderAction.Kind != actions.KindOpenFolder {
|
|
||||||
t.Errorf("action kind = %s", folderAction.Kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 11. List actions.
|
|
||||||
actionList, err := actionSvc.ListByNode(project.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("list actions: %v", err)
|
|
||||||
}
|
|
||||||
if len(actionList) != 2 {
|
|
||||||
t.Errorf("expected 2 actions, got %d", len(actionList))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12. Run an action (open URL — should not error).
|
|
||||||
_, err = actionSvc.Run(urlAction.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("run url action: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 13. Add worklog.
|
|
||||||
entry, err := worklogSvc.Add(project.ID, "Обновил витрину сайта, баннеры", "", 180, false, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("add worklog: %v", err)
|
|
||||||
}
|
|
||||||
if entry.Minutes == nil || *entry.Minutes != 180 {
|
|
||||||
t.Errorf("worklog minutes = %v, want 180", entry.Minutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 14. List worklog.
|
|
||||||
worklogList, err := worklogSvc.ListByNode(project.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("list worklog: %v", err)
|
|
||||||
}
|
|
||||||
if len(worklogList) != 1 {
|
|
||||||
t.Errorf("expected 1 worklog entry, got %d", len(worklogList))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 15. Search for something (FTS5 may not be available — gracefully skip).
|
|
||||||
_ = searchSvc.Rebuild()
|
|
||||||
_ = searchSvc.Index(noteNode.ID, noteNode.Title, content, notePath, "", "note")
|
|
||||||
results, err := searchSvc.Search("WordPress")
|
|
||||||
if err != nil {
|
|
||||||
_ = err
|
|
||||||
}
|
|
||||||
if len(results) > 0 {
|
|
||||||
t.Logf("search found %d results for 'WordPress'", len(results))
|
|
||||||
} else {
|
|
||||||
t.Log("search returned no results (FTS5 may not be available)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 16. Verify section filtering.
|
|
||||||
roots, err := nodeRepo.ListRoots(false, "clients")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("list roots by section: %v", err)
|
|
||||||
}
|
|
||||||
found := false
|
|
||||||
for _, r := range roots {
|
|
||||||
if r.ID == client.ID {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
t.Error("client not found in section 'clients'")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 17. Soft delete node and verify.
|
|
||||||
if err := nodeRepo.SoftDelete(project.ID); err != nil {
|
|
||||||
t.Fatalf("soft delete project: %v", err)
|
|
||||||
}
|
|
||||||
_, err = nodeRepo.GetActive(project.ID)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error getting deleted node")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 18. Reopen app (simulate by reconnecting to same DB).
|
|
||||||
db2, err := storage.Open(db.Path())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("reopen db: %v", err)
|
|
||||||
}
|
|
||||||
defer db2.Close()
|
|
||||||
repo2 := nodes.NewRepository(db2)
|
|
||||||
|
|
||||||
// 19. Verify data persists.
|
|
||||||
client2, err := repo2.GetActive(client.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("get client after reopen: %v", err)
|
|
||||||
}
|
|
||||||
if client2.Title != "ООО Ромашка" {
|
|
||||||
t.Errorf("client title after reopen = %q", client2.Title)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 20. Verify soft-deleted node stays soft-deleted.
|
|
||||||
allNodes, err := repo2.ListChildren(client.ID, true)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("list children after reopen: %v", err)
|
|
||||||
}
|
|
||||||
if len(allNodes) == 0 {
|
|
||||||
t.Error("no children found after reopen")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("MVP smoke test passed!")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMVPSmoke_WorklogReport(t *testing.T) {
|
|
||||||
db := openTestDB(t)
|
|
||||||
nodeRepo := nodes.NewRepository(db)
|
|
||||||
worklogSvc := worklog.NewService(db)
|
|
||||||
|
|
||||||
n, err := nodeRepo.Create("", nodes.TypeCase, "Test", "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = worklogSvc.Add(n.ID, "Task 1", "", 60, true, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = worklogSvc.Add(n.ID, "Task 2", "", 30, false, false)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
report, err := worklogSvc.Report(n.ID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("report: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(report, "Task 1") || !strings.Contains(report, "Task 2") {
|
|
||||||
t.Error("report missing entries")
|
|
||||||
}
|
|
||||||
if !strings.Contains(report, "Task 1") || !strings.Contains(report, "Task 2") {
|
|
||||||
t.Error("report missing tasks")
|
|
||||||
}
|
|
||||||
t.Logf("report:\n%s", report)
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue