Problem: MarkdownPreview dispatched DOM CustomEvent via link.dispatchEvent()
which doesn't propagate through Svelte's event system. The on:verstak-link
handler on <MarkdownPreview> in NoteEditorPanel only catches Svelte dispatch()
events, not DOM CustomEvents from {@html} content.
Fix: Replaced DOM CustomEvent dispatch with Svelte createEventDispatcher.
Now handleClick() in MarkdownPreview calls dispatch('verstak-link', {...})
which properly propagates through NoteEditorPanel → App.svelte chain.
Also: removed unused importInternalLink import (was unused after
InternalLinkPicker replaced the manual modal).
Root cause: DOMPurify afterSanitizeAttributes hook was treating verstak://
links as blocked because hash-based href didnt match ALLOWED_SCHEMES regex.
Fix:
1. afterSanitizeAttributes hook now checks data-verstak-href first and
returns early for internal links - they never get blocked
2. Changed href from hash-based to about:blank (safe value that
DOMPurify wont strip, unlike javascript:void(0))
3. Click handler already uses data-verstak-href, not href
Added unit test: markdown.test.js (27 tests for renderer.link output)
1. Fix RU_TO_EN keyboard map in keyboardLayout.ts:
- ч was mapped to 'c' (wrong), now correctly 'x'
- с was mapped to 'v' (wrong), now correctly 'c'
- ю was mapped to '.' (wrong), now correctly ','
- Full rewrite with standard QWERTY/ЙЦУКЕН positional mapping
- Examples: dthcnfr→верстак, руддщ→hello, цщкдв→world all correct now
2. Root cause of search breaking after 3-4 chars:
Old map had ч:'c', с:'v' swapped. So dthcnfr → 'верчиак' (wrong).
Each character was mapped to wrong Cyrillic equivalent.
3. Add unit tests: keyboardLayout.test.js (39 tests, node-runner):
- EN→RU: dthcnfr, ghbdtn, ntcn
- RU→EN: руддщ, цщкдв, ышеш
- Unicode safety: Latin c (U+0063) ≠ Cyrillic с (U+0441)
- expandKeyboardVariants for mixed inputs
- Edge cases: empty, single char, mixed case, numbers
4. InternalLinkPicker type tabs → filters (not search modes):
- Store rawResults (all) + filtered results by activeType
- Switching type tab no longer clears query or triggers new search
- Just filters existing rawResults by selected type
- Shows 'Нет результатов для этого типа' when filtered empty
5. Both GlobalSearch and InternalLinkPicker use same expandKeyboardVariants()
All tests PASS, full build OK.
Unified search normalization across InternalLinkPicker and GlobalSearch:
1. GlobalSearch.svelte: multi-variant search (same as InternalLinkPicker)
- expandKeyboardVariants() for RU/EN layout swap
- Parallel Search queries with dedup by type+nodeId+targetId+title
- 180ms debounce preserved
2. Backend: fix LOWER() in SQL for links/actions
- Replace LOWER(column) LIKE with lowercased columns (title_lower, url_lower, etc.)
- Migration 020: add lowercased columns + indexes for links and actions
- BackfillLinksLower() + BackfillActionsLower() in storage.go
- Update INSERT in bindings_links.go and action.go to populate lowercased columns
3. FTS5 search: Unicode case-insensitive
- Index lowercased title/content/tags in search_index
- sanitizeFTS() now lowercases query before MATCH
- RebuildFTS() called after migrations
4. Case-insensitive search for nodes (already done in previous commit, verified):
- title_lower column with Go strings.ToLower
- Search() queries title_lower with lowercased query
All test suites PASS, full build OK.
Replace broken ObjectPickerModal and manual modal with proper
InternalLinkPicker component:
- Search field with debounced SearchNodes API calls
- Type tabs: Дело, Заметка, Файл, Секрет (disabled)
- Results list showing title + path, keyboard navigation
- Inserts [Title](verstak://type/id) at cursor position
- No layout breakage — picker is a normal modal via position:fixed
- Escape/Cancel close picker cleanly
- bind:this on NoteEditorPanel → MarkdownEditor.insertText()
Also:
- MarkdownEditor: added public insertText() method + bind:this
- NoteEditorPanel: added bind:this on MarkdownEditor + public insertText()
- Removed manual modal, insertInternalLinkMarkdown(), document.querySelector
Use 'package-lock.json -nt node_modules/.package-lock.json' to detect
when lockfile changed (e.g. after git pull). Use 'npm ci' for
deterministic install instead of 'npm install'.
- withAuth skips check when server secret is empty
- flushQueue doesn't send X-Verstak-Secret header when secret is empty/undefined
- popup force-pings server on open (not relying on cached bridgeReachable)
- flushQueue updates bridgeReachable on every result
- Triple-state status: true/false/undefined→'Проверка...'
- Tests: verify events POST works without auth header, with empty header, full ping→events flow
rpmbuild runs %install from its own working directory where
is undefined. Copy artifacts into SOURCES and use
rpmbuild's %{_sourcedir} macro to locate them.
Replaced debug placeholder in render() with real calendar rendering:
header (nav + view tabs), categories bar with filter, and month/week/day
views. Full CRUD modals were already present in the HTML.
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 {}.
- Added debug prints to log params type in get_events
- Added type guard in backward compat path (reject non-string end_date)
- Added type guard for start_date/end_date before passing to SQL
Root cause: initVault() called Discover() but never called
SyncConfig(), InitRuntimes(), CallInitHooks(), or StartSchedulers().
On restart, plugins were discovered from disk but their config state
(Installed/Enabled) was not restored and no Lua VMs were created.
This caused:
- Settings → Plugins showing plugins as 'not installed' after restart
- Sidebar showing calendar item (from enabledSet config) but
GetPluginPanelHTML failing (only checked p.Active)
- Toggle working in-session but state lost on restart
- closeVault() not stopping plugin schedulers/shutdown/VM (leak)
Fixes:
- bindings_config.go: add SyncConfig(appCfg) after Discover()
- bindings_config.go: add InitRuntimes() + CallInitHooks() + StartSchedulers()
after watcher start
- bindings_config.go: add StopSchedulers + CallShutdownHooks + CloseRuntimes
to closeVault() (before db.Close since plugins use DB)
- bindings_plugins.go: GetPluginPanelHTML now checks enabledSet || p.Active,
consistent with ListSystemViewsWithPlugins and ListPlugins
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.
PluginPage.svelte использовал несуществующий Wails binding CallPluginAction.
Заменён на CallPluginFunction с правильным dotted path (calendar.get_events и т.д.),
что соответствует сигнатуре bindings_plugins.go.
Frontend пересобран, go build + go test ./... — всё зелёное.
- 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