8.1 KiB
8.1 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 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/
notes/
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, rename, import, worklog, inbox assign, link edit, settings, file preview
- Cross-cutting concerns: navigation history (goBack, rememberNavigation), keyboard shortcuts, drag-and-drop orchestration, capture/inbox flow
App.svelte is NOT responsible for:
- Individual tab content (Overview, Notes, Files) — these are inlined but should be extracted
- Note editor internals —
NoteEditorPanel.svelte+MarkdownEditor.sveltehandle this - Settings sections — each has its own
Settings*.svelte
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 - Journal state:
journalRows,journalSummary, filters - Worklog modal state:
showWorklogModal,wlModal* - Inbox:
inboxNodes,localInboxNodes, capture state - Links:
links - Sync:
syncStatus error(global error banner)
App.svelte must NOT directly own (after extraction):
- Files tab internal state →
FilesTab.svelte - Notes tab list UI state →
NotesTab.svelte - Overview tab content →
OverviewTab.svelte
Component Communication
Props (parent → child)
Data flows down via Svelte export let prop
Events (child → parent)
Children dispatch events via createEventDispatcher()
- Suffix
:in event names means handler in template:on:openNote={handler}, NOT call on mount - All events should use
e.detailto pass data
Public API (bind:this)
Parent gets imperative handle via bind:this={ref} and calls:
ref.publicMethod(args)— guard with optional chaining:ref?.publicMethod?.(args)- Methods are exposed via
export functionin child component
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