10 KiB
Frontend Architecture — Verstak GUI
Tech Stack
- Framework: Svelte 4 (runes-free, compiler-only)
- Build tool: Vite 5
- GUI shell: Wails v2 (Go backend + WebKit GTK frontend)
- Language: Plain JavaScript (no TypeScript in Svelte files —
lang="ts"is NOT used)
Source Structure
frontend/src/
main.js # Entry: mounts App.svelte, global error handlers
App.svelte # Root component: sidebar, header, modals, tab router
FileTreeRow.svelte # Sidebar tree node row
TreeNode.svelte # Recursive tree node
lib/
AppHeader.svelte # Top bar with title, search, actions
BrowserEvents.svelte # Browser extension events panel
CalendarPluginPage.svelte # Plugin page host
ConfirmModal.svelte # Reusable confirm dialog
FileBreadcrumbs.svelte # File tab breadcrumb navigation
FileIcon.svelte # Icon resolver for file types
FilePreviewModal.svelte # File content preview modal
FirstRun.svelte # First-run wizard
GlobalSearch.svelte # Global search bar + results
SettingsWindow.svelte # Settings modal container
Settings*.svelte # Settings sub-sections (General, Sync, etc.)
SyncStatus.svelte # Sync status badge
TemplateIcon.svelte # Template type icon
TodayScreen.svelte # Today dashboard
VaultRecovery.svelte # Vault recovery wizard
actionIcons.js # SVG icon strings
fileUtils.js # File type helpers (canPreview, isMarkdown, etc.)
i18n/ # Internationalization (en, ru)
markdown/ # Markdown rendering + internal links
util/ # Keyboard layout helper
components/
OverviewTab.svelte # Overview tab: meta, quick actions, recent items
files/
FilesTab.svelte # Files tab: file browser, preview, import, rename
notes/
NotesTab.svelte # Notes tab: note list, create form
MarkdownEditor.svelte # Markdown textarea with toolbar
MarkdownPreview.svelte # Rendered markdown preview
NoteEditorPanel.svelte # Editor + preview layout, public API: insertText()
InternalLinkPicker.svelte # Object picker for verstak:// links
ObjectPickerModal.svelte # Legacy object picker modal
Role of App.svelte
App.svelte is the root component. It owns:
- Global UI state: sidebar (system views, workspace tree), active tab, selected node/section
- Lifecycle: startup checks (GetStartupStatus, VerstakVersion, ListWorkspaceTree), event listeners
- Top-level modals: confirm, node rename, import (removed — now in FilesTab), worklog, inbox assign, link edit, settings, trash preview
- Cross-cutting concerns: navigation history (goBack, rememberNavigation), keyboard shortcuts, drag-and-drop orchestration, capture/inbox flow
- Note editor lifecycle:
noteEditorstate,doOpenNote(),saveCurrentNote(),closeNoteEditor(), link modal, internal link picker
App.svelte is NOT responsible for:
- Files tab →
FilesTab.svelte(owns all file browser state, preview, import, rename) - Notes tab list →
NotesTab.svelte(owns note list UI, create form; editor stays in App) - Overview tab →
OverviewTab.svelte(pure display: meta, quick actions, recent items) - Settings sections → each has its own
Settings*.svelte
Component Communication
Props (parent → child)
Data flows down via Svelte export let prop
Events (child → parent)
Children dispatch events via createEventDispatcher()
Public API (bind:this)
Parent gets imperative handle via bind:this={ref} and calls:
ref.publicMethod(args)— guard with optional chaining:ref?.method?.(args)
Component Reference
OverviewTab (lib/components/OverviewTab.svelte)
Props: selectedNode, notes, worklog, formatDate, nodeKindLabel
Events: createNote, addFile, createAction, switchTab, openNote
State: None (pure display)
NotesTab (lib/components/notes/NotesTab.svelte)
Props: notes, formatDate
Events: submitCreateNote({title}), openNote({note}), startRename({noteId, currentTitle}), deleteNote({note})
State: showCreateNote, newNoteTitle
FilesTab (lib/components/files/FilesTab.svelte)
Props: selectedNode, wailsCall
Events: openNote({id, title}), refreshParent({nodeId}), error({message})
Public API (via bind:this):
resetToNode(nodeId)— reset to root of given nodeaddFile()— open file picker and importloadFolder(folderId)— load folder contentsopenFileById(fileNodeId)— find and preview filefocusItem(nodeId)— select item by IDhandleFilesKeydown(e)— delegate keyboard handlingresetState()— full reset (on node change)
Internal state (owned by FilesTab, NOT accessible from App):
loadingFiles,currentFolderId,folderStack,fileItemspreviewItem,previewContent,previewLoading,previewErrorclipboard,selectedIds,dragIdsimporting,importSummary,showImportDialog,pendingImportPath,pendingImportParentshowRename,renameId,renameValue,renameErrorshowConfirm,confirmTitle,confirmMessage,confirmAction,cancelAction
State Ownership Rules
App.svelte MUST own:
selectedSection,selectedNode,activeTabsystemViews,workspaceTree,enabledTemplatesnoteEditor(the note being edited),noteViewModeshowFirstRun,showRecovery,showSettings,loadingstartupStatus,startupChecked- Navigation state:
navHistory,restoringHistory - Trash browser state:
trashInfo,trashCount,trashSelectedIds,trashFolderId,trashFolderStack - Trash preview:
trashPreviewItem,trashPreviewContent,trashPreviewLoading,trashPreviewError - Journal state:
journalRows,journalSummary, filters - Worklog modal state:
showWorklogModal,wlModal* - Inbox:
inboxNodes,localInboxNodes, capture state - Links:
links - Sync:
syncStatus error(global error banner)notes(loaded byloadTabData, passed to NotesTab and OverviewTab as prop)worklog(loaded byloadTabData, passed to OverviewTab as prop)
App.svelte must NOT directly reference:
- Files tab internal state (all in FilesTab)
- Notes tab create form state (showCreateNote, newNoteTitle — in NotesTab)
- Any
fileItems,selectedIds,currentFolderId,folderStack, etc.
Services (Wails API)
All backend calls go through the wailsCall() helper in App.svelte:
function wailsCall(method, ...args) {
// Returns Promise, rejects with 'Wails not connected: <method>' if backend unavailable
}
Key backend methods:
- Startup:
GetStartupStatus,VerstakVersion,ListSystemViewsWithPlugins,ListWorkspaceTree,ListEnabledTemplates - Nodes:
GetNodeDetail,ListItems,ListWorkspaceChildren,CreateNodeFromTemplate,DeleteNode,MoveNode,RenameNode - Notes:
ListNotes,CreateNote,ReadNote,SaveNote,DeleteNote,RenameNote - Files:
ListFiles,DeleteFileOrFolder,CreateEmptyFile,DuplicateNode,OpenFile,OpenFolder,GetFileBase64,ReadFileText,PreviewImport,AddPathCopy,AddPathLink - Worklog:
ListWorklog,CreateWorklogFull,UpdateWorklogEntry,DeleteWorklogEntry,AcceptSuggestionFull/With,GetSuggestions - Inbox:
ListInboxNodes,Capture*WithContext,DeleteInboxNode,ResolveInboxNode - Sync:
SyncStatus,SyncNow,TrashCount,ListTrash - Logging:
FrontendLog(level, message, stack),LogStartupStep(step, success, detail)
Logging & Diagnostics
Frontend errors
- Global
window.addEventListener('error', ...)catches uncaught exceptions - Global
window.addEventListener('unhandledrejection', ...)catches broken promises - Both log to
console.errorand forward toFrontendLog()backend binding - Fatal mount error: renders error message inline in
#appdiv (blank window becomes diagnostic)
Backend logs
- Application log file:
~/.local/state/verstak/logs/verstak.log(Linux) - Fallback:
~/.config/verstak/logs/verstak.log - Format:
[timestamp] [level] message - Go
log.Printf()output goes to stderr (visible in dev console)
Vault debug log
<vault>/.verstak/debug.log— written byWriteDebugLogbinding (existing feature)
Refactor Rules
After each extraction step:
grepApp.svelte for state that moved to child — must be zero hits or documented exceptionsnpm run build— must passgo test ./...— must pass- Manual GUI check:
- Window is not blank/white
- Sidebar shows (system views + workspace tree)
- No infinite "Загрузка..." on welcome screen
- Clicking system sections works
- Opening a case shows tabs
Forbidden patterns:
- Child component state referenced directly in App.svelte after extraction
lang="ts"in Svelte files (svelte-preprocess not installed)[]stringpassed directly to Wails bindings (must JSON-serialize)- Modal without
role="dialog" aria-modal="true"and Escape handler - Buttons without
type="button"in forms resize: noneon textareas that should be resizable (useresize: vertical)
Troubleshooting
Blank window, no errors in terminal
- Check browser DevTools console (F12 on Windows/Linux, Cmd+Alt+I on macOS)
- In Wails dev mode: right-click → Inspect → Console
- Look for
[Frontend Error]messages - Check application log:
~/.local/state/verstak/logs/verstak.log
Stuck on "Загрузка..."
GetStartupStatuslikely failed or returned unexpected status- Check network calls in DevTools Network tab
- Check backend log for startup errors
Wails method not found
- Ensure method is registered in
bindings*.gofiles - Ensure method is exported (capitalized name)
- Ensure
wailsCall()helper is used (not direct window access)
Child component state moved but App still references old variable
- This causes a silent
undefinedreference or runtime crash - Run
grep -n 'oldStateName' App.svelteafter each extraction - Use
ref?.method?.()for all public API calls on child components