- resolveActivityTarget uses 'targetId'/'targetPath' for all types
(note, file, folder) instead of type-specific property names
- openActivityTarget for files: resolves the file node via
GetNodeDetail, navigates to its parent folder in the Files tab,
then auto-previews the file if it's a previewable type
- For root-level files (no parent_id): loads root items
- Removed spurious OpenFolder(targetPath) call that silently failed
because OpenFolder expects a node ID, not a filesystem path
selectNode() resets fileItems=[] and activeTab='overview'. Setting
activeTab='files' programmatically does not trigger the tab click
handler that calls loadFolder(), so the file tree stays empty.
Fix: explicitly call await loadFolder(selectedNode.id) in the files
branch of openActivityTarget.
Also: unified resolveActivityTarget return shape to always use
targetId/targetPath regardless of targetType.
- New cmd/verstak-gui/bindings_debug.go: WriteDebugLog(msg) appends a
timestamped line to the vault's .verstak/debug.log
- Frontend: console.log replaced with writeDebugLog() which calls
wailsCall('WriteDebugLog', ...) — works in production Wails builds
- Both acceptTodaySuggestion and acceptJournalSuggestion log eventIds,
events, and errors to the file
Tests added to suggest_test.go:
- TestJournalFullRegression: GetSuggestions -> verify eventIds match
events -> AcceptSuggestionWith -> verify all 3 linked via
worklog_entry_events + JOIN + GetWorklogEntryEvents
- TestSuggestionOnRepeatedActivity: first event accepted, new event
created -> GetSuggestions still returns suggestion for the new event
- TestManualWorklogEntry: CreateWorklogFull -> source=manual,
billable/approximate/details preserved, GetWorklogEntryEvents empty
Frontend:
- resolveActivityTarget(ev) pure function returning { nodeId, tab,
noteId/fileId/targetPath } based on targetType
- openActivityTarget(ev) uses resolveActivityTarget for navigation
Root cause: Wails v2.12.0 cannot reliably marshal []string arguments
from JavaScript to Go when called through positional binding.
The event IDs array arrived empty on the Go side, causing no
worklog_entry_events INSERTs.
Fix:
- AcceptSuggestionWith now accepts eventIDsJSON (string) instead of
eventIDs ([]string). Frontend passes JSON.stringify(eventIds).
- Backend json.Unmarshal into []string before validation.
- Pre-insert validation: each eventID checked in activity_events.
- Atomic tx: entry create + linking in single Begin/Commit.
- INSERT (not INSERT OR IGNORE) — failure is a hard error.
- Post-commit verification: JOIN COUNT(*) must match len(eventIDs).
- End-to-end test: TestAcceptSuggestionWithEndToEnd creates a node,
3 activity events, accepts suggestion, verifies all 3 linked.
Other changes:
- GetWorklogEntryEvents: fixed column name (details_json -> metadata).
- openActivityTarget(ev): new function for 'Посмотреть' button that
navigates to specific note/file/folder instead of just opening node.
- All 'openNodeById(ev.nodeId)' in event contexts replaced with
'openActivityTarget(ev)'.
Root cause: s.eventIds may be undefined in JavaScript even when s.events
has data (Wails v2 marshalling of []string in nested struct response).
On calling AcceptSuggestionWith(eventIDs []string), empty array reached Go,
no INSERTs executed, events silently lost.
Changes:
- Frontend: extractEventIds() fallback — s.eventIds || s.events[].id || []
- Frontend: console.log debug for eventIds/events in accept handler
- Backend: AcceptSuggestionWith wrapped in tx (Begin/Commit/Rollback) so
entry creation + event linking is atomic
- Backend: AddWithSourceTx method for transaction-aware insert
- Backend: buildEntry helper extracted
- Backend: fmt.Printf debug logging for received eventIDs + link count
- Backend: verification query after commit
- Cleanup: removed stale frontend-dist assets, .gitignore build.log
- AcceptSuggestionWith now accepts nodeID, summary, minutes, date, eventIDs
as separate args instead of the entire Suggestion struct (Wails v2 skips
nested struct fields during JS→Go marshalling)
- Error handling: event link failures now return an error instead of silent ignore
- Event type labels in suggestion detail and journal row detail now use
eventLabel() which maps snake_case types to human-readable i18n labels
(e.g. note_updated → 'Заметка изменена')
- Added missing event labels: note_deleted, node_deleted, folder_moved,
action_created, action_done, worklog_added
- Add source column to worklog_entries (migration 014): manual/suggestion/unknown
- GetSuggestions now excludes only events linked in worklog_entry_events,
not entire nodes — repeated activity same day now produces suggestions
- Manual entry form replaced with '+' button + modal dialog
- Source display shows correct origin (manual/suggestion/unknown/no-events)
- Include-children checkbox hidden when no node selected
- Activity events navigate to specific notes/files instead of just case
- Expandable row reactivity fixed (journalRows/worklog reassignment)
- node picker: Search/Path on Repository, SearchNodes binding,
debounced search dropdown showing title + full path
- ByNode summary groups by nodeID with NodePath as label (not NodeTitle)
- PDF export for worklog reports with embedded DejaVuSans fonts
- ExportWorklogPDF binding + button on Journal screen
- Removed unused Section field from ReportFilter
- ListReport now calls BuildReportPaths so nodePath is available
- go.sum: +github.com/signintech/gopdf dependency
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
cmd/verstak-server/server.go (2838→127 строк): разделён на 12 файлов
- config.go, tokens.go, schema.go
- server.go (только struct + NewServer + ListenAndServe)
- routes.go, middleware.go, smtp.go
- handlers_api.go, handlers_user.go, handlers_web_user.go, handlers_admin.go
- templates.go (конвертирован в функции с i18n.T())
frontend: все русские строки заменены на t() вызовы
- App.svelte, FileTreeRow.svelte, ConfirmModal.svelte
- FilePreviewModal.svelte, fileUtils.js
core: gofmt по всему проекту
Все сборки (CLI, server, gui, frontend), go vet, go test проходят.
check-i18n.sh: frontend чист, Go-файлы с кириллицей — только тесты/легаси.
- 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
- Add handleAuthTest endpoint that validates credentials without creating a device
- Fix resetPasswordHTML to use {TOKEN} placeholder instead of %s
- Remove fmt.Sprintf from admin dashboard (no format args needed)
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