verstak/docs/frontend-architecture.md

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:

  1. Global UI state: sidebar (system views, workspace tree), active tab, selected node/section
  2. Lifecycle: startup checks (GetStartupStatus, VerstakVersion, ListWorkspaceTree), event listeners
  3. Top-level modals: confirm, node rename, import (removed — now in FilesTab), worklog, inbox assign, link edit, settings, trash preview
  4. Cross-cutting concerns: navigation history (goBack, rememberNavigation), keyboard shortcuts, drag-and-drop orchestration, capture/inbox flow
  5. Note editor lifecycle: noteEditor state, doOpenNote(), saveCurrentNote(), closeNoteEditor(), link modal, internal link picker

App.svelte is NOT responsible for:

  • Files tabFilesTab.svelte (owns all file browser state, preview, import, rename)
  • Notes tab listNotesTab.svelte (owns note list UI, create form; editor stays in App)
  • Overview tabOverviewTab.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 node
  • addFile() — open file picker and import
  • loadFolder(folderId) — load folder contents
  • openFileById(fileNodeId) — find and preview file
  • focusItem(nodeId) — select item by ID
  • handleFilesKeydown(e) — delegate keyboard handling
  • resetState() — full reset (on node change)

Internal state (owned by FilesTab, NOT accessible from App):

  • loadingFiles, currentFolderId, folderStack, fileItems
  • previewItem, previewContent, previewLoading, previewError
  • clipboard, selectedIds, dragIds
  • importing, importSummary, showImportDialog, pendingImportPath, pendingImportParent
  • showRename, renameId, renameValue, renameError
  • showConfirm, confirmTitle, confirmMessage, confirmAction, cancelAction

State Ownership Rules

App.svelte MUST own:

  • selectedSection, selectedNode, activeTab
  • systemViews, workspaceTree, enabledTemplates
  • noteEditor (the note being edited), noteViewMode
  • showFirstRun, showRecovery, showSettings, loading
  • startupStatus, 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 by loadTabData, passed to NotesTab and OverviewTab as prop)
  • worklog (loaded by loadTabData, 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.error and forward to FrontendLog() backend binding
  • Fatal mount error: renders error message inline in #app div (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 by WriteDebugLog binding (existing feature)

Refactor Rules

After each extraction step:

  1. grep App.svelte for state that moved to child — must be zero hits or documented exceptions
  2. npm run build — must pass
  3. go test ./... — must pass
  4. 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)
  • []string passed directly to Wails bindings (must JSON-serialize)
  • Modal without role="dialog" aria-modal="true" and Escape handler
  • Buttons without type="button" in forms
  • resize: none on textareas that should be resizable (use resize: vertical)

Troubleshooting

Blank window, no errors in terminal

  1. Check browser DevTools console (F12 on Windows/Linux, Cmd+Alt+I on macOS)
  2. In Wails dev mode: right-click → Inspect → Console
  3. Look for [Frontend Error] messages
  4. Check application log: ~/.local/state/verstak/logs/verstak.log

Stuck on "Загрузка..."

  • GetStartupStatus likely 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*.go files
  • 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 undefined reference or runtime crash
  • Run grep -n 'oldStateName' App.svelte after each extraction
  • Use ref?.method?.() for all public API calls on child components