chore(frontend): add diagnostics before app split
- main.js: global error handlers (error + unhandledrejection), try/catch around App mount with inline fallback UI - bindings_logging.go: FrontendLog() and LogStartupStep() backend bindings for persistent application logging - App.js: FrontendLog() and LogStartupStep() Wails JS bindings - docs/frontend-architecture.md: component architecture, state ownership rules, communication patterns, logging guide, troubleshooting All existing code untouched. This is a pure additive step that enables blank-window debugging before any App.svelte extraction begins.
This commit is contained in:
parent
bfe57ac0ac
commit
df21340402
|
|
@ -0,0 +1,89 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"verstak/internal/core/config"
|
||||
)
|
||||
|
||||
// appLogPath returns the path to the application log file.
|
||||
// Uses ~/.local/state/verstak/logs/verstak.log on Linux,
|
||||
// or falls back to ~/.config/verstak/logs/verstak.log.
|
||||
func appLogPath() string {
|
||||
// Prefer XDG_STATE_HOME, then fallback to config dir
|
||||
stateDir := os.Getenv("XDG_STATE_HOME")
|
||||
if stateDir == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
stateDir = filepath.Join(home, ".local", "state")
|
||||
}
|
||||
}
|
||||
if stateDir != "" {
|
||||
dir := filepath.Join(stateDir, "verstak", "logs")
|
||||
if err := os.MkdirAll(dir, 0o755); err == nil {
|
||||
return filepath.Join(dir, "verstak.log")
|
||||
}
|
||||
}
|
||||
// Fallback to config dir
|
||||
cfgDir, err := config.EnsureConfigDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
dir := filepath.Join(cfgDir, "logs")
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return ""
|
||||
}
|
||||
return filepath.Join(dir, "verstak.log")
|
||||
}
|
||||
|
||||
// appLog writes a timestamped line to the application log file.
|
||||
func appLog(level, msg string) {
|
||||
logPath := appLogPath()
|
||||
if logPath == "" {
|
||||
log.Printf("[%s] %s", level, msg)
|
||||
return
|
||||
}
|
||||
line := fmt.Sprintf("[%s] [%s] %s\n", time.Now().Format("2006-01-02T15:04:05"), level, msg)
|
||||
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
log.Printf("[%s] %s", level, msg)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
f.WriteString(line)
|
||||
}
|
||||
|
||||
// FrontendLog receives log messages from the frontend runtime.
|
||||
// Called from JS via Wails binding. Works even if vault is not open.
|
||||
// level: "info", "warn", "error"
|
||||
// message: human-readable message
|
||||
// stack: JS stack trace (optional, only for errors)
|
||||
func (a *App) FrontendLog(level, message, stack string) {
|
||||
msg := "[frontend] " + message
|
||||
if stack != "" {
|
||||
msg += "\n stack: " + stack
|
||||
}
|
||||
// Always log to Go's standard logger (visible in dev mode console)
|
||||
log.Printf("[frontend][%s] %s", level, message)
|
||||
// Persist to log file
|
||||
appLog("frontend-"+level, msg)
|
||||
}
|
||||
|
||||
// LogStartupStep logs a startup diagnostic step.
|
||||
// Called from Go side to trace initialization progress.
|
||||
func (a *App) LogStartupStep(step string, success bool, detail string) {
|
||||
status := "ok"
|
||||
if !success {
|
||||
status = "fail"
|
||||
}
|
||||
msg := fmt.Sprintf("[startup] %s: %s", step, status)
|
||||
if detail != "" {
|
||||
msg += " — " + detail
|
||||
}
|
||||
log.Print(msg)
|
||||
appLog("startup", msg)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -19,7 +19,7 @@
|
|||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-CzfuqGWF.js"></script>
|
||||
<script type="module" crossorigin src="/assets/main-Da3BSkUM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
# 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:
|
||||
|
||||
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, rename, import, worklog, inbox assign, link edit, settings, file preview
|
||||
4. **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.svelte` handle this
|
||||
- Settings sections — each has its own `Settings*.svelte`
|
||||
|
||||
## 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`
|
||||
- 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.detail` to 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 function` in child component
|
||||
|
||||
## Services (Wails API)
|
||||
|
||||
All backend calls go through the `wailsCall()` helper in App.svelte:
|
||||
|
||||
```js
|
||||
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
|
||||
|
|
@ -1,5 +1,48 @@
|
|||
import App from './App.svelte'
|
||||
|
||||
new App({
|
||||
target: document.getElementById('app')
|
||||
// ===== Global frontend error diagnostics =====
|
||||
// These catch runtime errors that would otherwise cause a silent blank window.
|
||||
|
||||
window.addEventListener('error', (event) => {
|
||||
const msg = event.error ? String(event.error) : event.message
|
||||
const stack = event.error && event.error.stack ? event.error.stack : ''
|
||||
console.error('[Frontend Error]', msg, stack)
|
||||
try {
|
||||
if (window['go'] && window['go']['main'] && window['go']['main']['App'] && typeof window['go']['main']['App']['FrontendLog'] === 'function') {
|
||||
window['go']['main']['App']['FrontendLog']('error', msg, stack)
|
||||
}
|
||||
} catch (e) {
|
||||
// Backend not ready — ignore
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const reason = event.reason
|
||||
const msg = reason instanceof Error ? String(reason) : JSON.stringify(reason)
|
||||
const stack = reason instanceof Error && reason.stack ? reason.stack : ''
|
||||
console.error('[Unhandled Promise Rejection]', msg, stack)
|
||||
try {
|
||||
if (window['go'] && window['go']['main'] && window['go']['main']['App'] && typeof window['go']['main']['App']['FrontendLog'] === 'function') {
|
||||
window['go']['main']['App']['FrontendLog']('error', 'Unhandled rejection: ' + msg, stack)
|
||||
}
|
||||
} catch (e) {
|
||||
// Backend not ready — ignore
|
||||
}
|
||||
})
|
||||
|
||||
// Mount App with error fallback
|
||||
try {
|
||||
new App({
|
||||
target: document.getElementById('app')
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[Fatal] App mount failed:', e)
|
||||
const el = document.getElementById('app')
|
||||
if (el) {
|
||||
el.innerHTML = '<div style="padding:24px;color:#ff6b6b;font-family:monospace;white-space:pre-wrap">'
|
||||
+ '<h2>Frontend Runtime Error</h2>'
|
||||
+ '<p>' + String(e) + '</p>'
|
||||
+ (e.stack ? '<pre>' + e.stack + '</pre>' : '')
|
||||
+ '</div>'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,5 +243,25 @@ export function CountActivityByNode(arg1) {
|
|||
}
|
||||
|
||||
export function CreateEmptyFile(arg1, arg2) {
|
||||
return window['go']['main']['App']['CreateEmptyFile'](arg1, arg2);
|
||||
return window['go']['main']['App']['CreateEmptyFile'](arg1, arg2)
|
||||
}
|
||||
|
||||
// ===== Logging & Diagnostics =====
|
||||
|
||||
export function FrontendLog(level, message, stack) {
|
||||
try {
|
||||
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
|
||||
return window['go']['main']['App']['FrontendLog'](level, message, stack || '')
|
||||
}
|
||||
} catch (e) {}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export function LogStartupStep(step, success, detail) {
|
||||
try {
|
||||
if (window['go'] && window['go']['main'] && window['go']['main']['App']) {
|
||||
return window['go']['main']['App']['LogStartupStep'](step, success, detail || '')
|
||||
}
|
||||
} catch (e) {}
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue