Backend:
- Fix MoveNode validation: wouldCreateCycle walks from newParentID up
toward root, rejects if nodeID is encountered (parent into descendant)
- Allow moving descendant to ancestor (C into A) and child to root
- Add isContainerType validation for new parent
- Add 8 tests covering all scenarios + duplicate ID invariant
Frontend TreeNode.svelte:
- Build parent map from full tree (not just loaded children)
- canDrop uses parent map for cycle detection
- Reactive drop-valid/drop-invalid CSS via pre-computed dropAllowed map
- Keyed {#each nodes as node (node.id)} for correct identity tracking
- Auto-expand container on 600ms drag-over hover
- Proper dragleave detection (ignore transitions to child elements)
- Clean up state on dragend
Frontend App.svelte:
- reloadTreePreservingExpanded: fresh roots + children (no patching)
- Root area visual drop indicator (dashed outline)
- dragleave handler for root area
Clean up stale GUI dist assets
- Fix tab highlight not updating visually — switch from class={tabClass()}
to Svelte's class:active directive for proper reactive class binding
- Rewrite README.md with full project structure, architecture, build guide
- Rewrite build.sh to build both GUI and server, output to build/
- Add scripts/build.sh for granular builds (gui/server/all)
- Add build/, frontend-dist/, and test vault dirs to .gitignore
- Remove stale binaries from project root
- Update AGENTS.md session summary
- FileTreeRow handleShowInFolder now closes the menu (menuOpen = false)
- OpenFolder: TypeFile nodes with empty FsPath fall back to file record path
- Active tab background increased to rgba(99,102,241,0.12), weight 600, border #818cf8
Sidebar refresh:
- addFile/addFolder now use currentFolderId || selectedNode.id as parent
- startImport stores pendingImportParent; confirmImport uses it
- setNodeChildren also updates node.has_children for toggle arrow reactivity
- navigateToFolder expands the folder node in sidebar tree
Context menu position:
- FileTreeRow stores menuX/menuY from contextmenu event coords
- Menu uses position:fixed with cursor-relative left/top and window clamp
Show in explorer:
- Sidebar context menu uses file.showInExplorer label
- en.js: add file.showInExplorer key
- reloadTreePreservingExpanded no longer replaces the whole tree,
only patches children of expanded nodes in-place
- New refreshParentNode(nodeId) function updates a single parent's children
- createFile, duplicateItem, confirmImport use refreshParentNode instead of reloadTreePreservingExpanded
- No intermediate render where children are lost
- Call reloadTreePreservingExpanded after createFile, confirmImport, duplicateItem
- New folders created inside a case now appear in sidebar without restart
- Add AGENTS.md with build instructions
- Backend: add HasChildren field to NodeDTO; ListWorkspaceTree/ListChildren
populate it by querying CountChildren for container types
- Node repository: add CountChildren(parentID, types...) method
- TreeNode: toggle shown only when has_children (not isContainer); double-click
on row = expand/collapse; icon click = expand/collapse; drop-valid class via
DOM classList for DnD highlight; auto-expand collapsed container on 500ms
hover during drag; auto-scroll near edge during drag
- App.svelte: selectNode no longer resets workspaceTree/expanded; new
reloadTreePreservingExpanded() helper re-fetches children for all expanded
nodes after DnD move / delete; deleteWorkspaceNode preserves expanded state
- Backend: ListWorkspaceTree/ListWorkspaceChildren filter to container types
only (case, client, project, folder, document, recipe)
- TreeNode: full-row context menu (removed label stopPropagation),
double-click toggles expand, icon-click toggles expand, DnD auto-expand
on 500ms hover, auto-scroll near edges, drag-over highlight via classList
- App.svelte: toggleExpand uses ListWorkspaceChildren, submitCreateNode uses
ListWorkspaceChildren for child tree population
- Note/file nodes no longer appear in the sidebar workspace tree
TreeNode.svelte:
- Native HTML5 drag-and-drop with move effect
- Lazy tree expand/collapse (arrow for container types only)
- Drop validation: no self-drop, container-only, descendant check
- 'case' icon kind added
App.svelte:
- toggleExpand loads children via ListChildren into tree
- handleNodeDrop calls MoveNode(draggedId, targetId), refreshes tree
- Root workspace area is a drop target (handleDropRoot)
- Overview section shows nodeKindLabel instead of raw type enum
- Context menu shows Create only for container types
- Create modal title uses 'Создать элемент'
- submitCreateNode expands parent after child creation
TemplateIcon.svelte: added 'case' icon (folder-like with dividers)
i18n: added nav.createNode key (ru+en)
- TreeNode.svelte: no white spacer for leaf nodes, iconKind maps node.type
directly, 32px rows with hover/selected states
- App.svelte: header shows localized nodeKindLabel, auto-select created
node, create modal with Пустое дело card + template descriptions,
disable button until name+type chosen
- i18n: add kind.folder/note/file, template descriptions,
template.optionNone → Пустое дело / Empty case
- applyRemoteNodeUpdate: FS-first with SafeVaultPath validation, must-fail os.Rename
- applyRemoteNodeMove: FS-first for folders and notes/files
- moveNodeFiles: rewritten FS-first with atomic DB transaction
- applyRemoteNoteMove: delegates to moveNodeFiles
- CreateNodeFromTemplate: rollbackChildren on any child creation failure
- DeleteToTrash: skip rename if source file already missing
- DeleteNodeAndChildren: fail on deleteFileRecords errors and trash move failures
- docs/PLAN.md: update step 14 status with known gaps
- notes.Create(): .md files stored in parent node's fs_path folder
- files.CopyIntoVault/CreateEmptyFile/Duplicate: use parent fs_path
- files.AddPathCopy/AddPathLink: use parent fs_path, set folder fs_path
- files.DeleteNodeAndChildren: move physical folder to .verstak/trash
- UpdateFsPathRecursive: use SafeDisplayNameToPathSegment(child.Title)
- sync_apply.go note ops: use fs_path instead of spaces/
- internal/gui/server.go file upload: use n.FsPath instead of nodeSlug
- VaultCheck diagnostic: walk nodes/files, verify paths on disk
- Tests: create/rename/move/delete/name-conflict/vault-check all pass
- applyRemoteNoteUpdate: use SafeVaultPath for vault mode, skip non-vault with log
- Email subjects/bodies moved to Go i18n (confirm + reset) in ru.json and en.json
- check-i18n.sh: ru/en key mismatch now FAIL (not WARNING)
- All builds, tests, frontend pass
- Expand all RecordOp payloads with full entity data needed for reconstruction
- Add applyRemoteOp with handlers for node/note/file/action/worklog CRUD
- SyncNow() now applies pulled ops to real DB tables + sets LastSeenServerSeq
- SyncTestConnection uses /api/auth/test instead of PairDevice
- autoSyncLoop respects configured interval with lastSync timing
- Add helper functions: nodePayload, notePayload, filePayload, actionPayload, worklogPayload
BREAKING: replace legacy API keys with device tokens via pairing flow.
- Server: /api/client/pair, revoke, me endpoints; server_sequence + tombstones + idempotency
- Desktop client: PairDevice, GetMe, RevokeCurrent; auto-sync loop every 60s
- Config: device_token stored in separate file (0600), not config.yml
- Client DB: last_pull_seq migration for incremental pull
- Frontend (Svelte): settings modal with connect/disconnect/interval
- User dashboard (/dashboard): device list with status, revoke with password
- Admin dashboard (/admin/dashboard): devices table from /admin/api/devices
- CLI (cmd/verstak): updated for ServerSequence/GetState changes
- Fix: autoSyncLoop falls back to SQLite sync_state for server URL
- Fix: SyncSetInterval preserves server_url/device_id from SQLite
- internal/core/sync/: Service, Client, Blob packages
- RecordOp creates sync_ops entries for all mutations
- Client for push/pull/blob HTTP to server
- Blob SHA-256 hashing and local storage
- Wired into app.go alongside activity recording
- Device ID from config or fallback
TodayView now uses only activity_events (source of truth) + ListTodayNodes (ensure changed cases appear). Removed direct queries to nodes (notes) and files tables — those will come from activity_events going forward.
- ListTodayView() on backend: queries nodes created or updated today
using local timezone day boundaries
- todayBoundaries() helper returns start/end of current day in RFC3339
- Section validation: Create() rejects today and inbox as node sections
- validSections moved from repository.go to types.go with IsValidSection
and IsServiceSection helpers
- Frontend selectSection('today') calls ListTodayView instead of
ListNodesBySection('today')
- FAB (create node) hidden in today and inbox sections
- CreateNode in app.go guarded against today/inbox sections
- types.go: today/inbox defined as service sections (sidebar only)
Backend fixes:
- Wire search service in App struct, implement Search() bindings
- Fix OpenFile to use files.Service.Open() instead of stub
- Fix OpenFolder to open spaces/<slug>/ instead of vault root
- Remove unused imports and dead code in app.go
Frontend fixes:
- Add missing Svelte plugin to vite.config.js (blocking build error)
- Fix optional catch binding for compatibility
- Fix select dropdown rendering on Linux (appearance: none + custom arrow)
- Switch api/verstak.js to use generated Wails v2 bindings
- Include hand-written wailsjs bindings in repository
- Add build.sh to repository
Build:
cd frontend && npm run build
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
Problem: UI appeared as narrow dark panel on white background with scrollbar.
Root causes:
- html/body had no reset — default browser margin/padding = white borders
- index.html referenced non-existent /style.css
- .app used height:100vh but no width:100vw or overflow:hidden
- sidebar used fixed width instead of flexbox
Fixed:
- index.html: added critical CSS reset (html/body/#app = 100%, margin:0, overflow:hidden, dark bg)
- index.html: removed /style.css ref, changed lang to ru, title to Верстак
- App.svelte: complete layout rewrite
- .app = flex, 100vw x 100vh, overflow:hidden
- sidebar = 260px width, full height, flex column
- main = flex:1, full height, flex column (header + content)
- sidebar nav with sections + nodes using <button> elements
- content area fills remaining space
- proper dark theme colors throughout
- Rebuilt frontend/dist and cmd/verstak-gui/frontend-dist