Empty Lua tables from DB queries (e.g. get_events with no results)
are ambiguous — they could be [] or {}. Frontend expects arrays
(with .length), so we default empty tables to [] instead of {}.
Root cause: CallFunctionJSON used .String() on Lua return values, which
for tables produces 'table: 0x...' — not valid JSON. Frontend does
JSON.parse() on the result and silently caught the parse error.
Fix:
- runtime.go: convert Lua return value to JSON via luaValueToGo +
json.Marshal so tables become proper JSON arrays/objects
- main.lua: add backward compat in get_events() and update_event()
to accept both positional args (start, end) and table params
- CalendarPluginPage.svelte: show errors in UI instead of silent catch;
restructure template to always show iframe + error overlay
1. SetPluginEnabled(true): after DeactivatePlugin, also call Disable(name)
to rollback in-memory Enabled state (not just config).
2. on_init failure is now fatal for ActivatePlugin — returns error
and rolls back scheduler + VM (was incorrectly non-fatal).
3. TestSetPluginEnabled_BrokenPlugin_Rollback: end-to-end test with
broken plugin (invalid interval), verifies error + not Active +
not Enabled + not in config.
- Discover(): skip plugins without on_install hook (not shown in UI)
- Enable(): simplified - just check Installed flag
- SyncConfig(): simplified - no HasInstall special case
- Frontend: simplified toggle logic (only installed/uninstalled states)
- Tests: updated all test fixtures to include on_install hook
- Plugins without on_install hook: always 'installed', toggle works directly
- Plugins with on_install: must Install first, then toggle, then Uninstall available
- No data cleanup on Disable (only on Uninstall via on_uninstall hook)
- Old plugins without lifecycle hooks simply don't get install/uninstall UI
Critical:
- bridge: AutoGenPort=false по умолчанию, не генерируем secret если пустой
→ extension и bridge совпадают на port 9786 и empty secret
- bridgeConfig: убрана авто-генерация secret, убран secret из BridgeInfo
High:
- extension/background.js + extension-firefox/background.js:
все chrome.* listeners вынесены в global scope (не внутри onInstalled/onStartup)
→ MV3 service worker корректно перезапускается
- UI: acceptBrowserEvent вызывает AcceptBrowserEvent, attachBrowserEvent вызывает
AttachBrowserEventToNode (к текущему selectedNode), а не DismissBrowserEvent
- watcher: при Create проверяется isUnderVault(absPath, vaultRoot) —
если файл уже в vault, используется AddExternal вместо CopyIntoVault
→ нет дублирования файлов с timestamp-суффиксом
Medium:
- bridge.Event: добавлено поле DeviceID, handleEvents обогащает events из batch.DeviceID
→ device_id сохраняется в DB как chrome-*/firefox-*, а не evt_*
- config: FileWatcher изменён на *bool — nil означает default true,
false = явно выключено → старые config.json без поля file_watcher получают true
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
- 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: 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
- 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
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-файлы с кириллицей — только тесты/легаси.
TestE2ESync starts a real server process, registers user, pairs two devices,
pushes a node op from client A, pulls on client B, verifies payload content,
and confirms /api/auth/test does not create devices.
- Add ClientSequence and LastSeenServerSeq fields to sync.Op struct
- Fix Client.Push() to copy these fields to PushOp
- Add GetDeviceID() method to sync.Service