Compare commits
8 Commits
c3ee4cc21f
...
8366815cf2
| Author | SHA1 | Date |
|---|---|---|
|
|
8366815cf2 | |
|
|
a7563b6016 | |
|
|
139b2a67b7 | |
|
|
f59af601d6 | |
|
|
a6d349f188 | |
|
|
acabe340e9 | |
|
|
9b3f3b6afc | |
|
|
85a947bc73 |
|
|
@ -46,6 +46,13 @@ official.notes/
|
||||||
"apiVersion": "1",
|
"apiVersion": "1",
|
||||||
"description": "Markdown notes inside Verstak cases.",
|
"description": "Markdown notes inside Verstak cases.",
|
||||||
"source": "official",
|
"source": "official",
|
||||||
|
"localization": {
|
||||||
|
"defaultLocale": "en",
|
||||||
|
"locales": {
|
||||||
|
"en": "locales/en.json",
|
||||||
|
"ru": "locales/ru.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
"provides": [
|
"provides": [
|
||||||
"workspace.notes",
|
"workspace.notes",
|
||||||
"entity.note"
|
"entity.note"
|
||||||
|
|
@ -85,6 +92,35 @@ official.notes/
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3.1. Локализация
|
||||||
|
|
||||||
|
Локализуемый плагин объявляет собственные JSON-каталоги в `localization`.
|
||||||
|
Пути должны быть относительными, использовать `/` и оставаться внутри каталога
|
||||||
|
плагина. Все значения каталога — строки. Литеральные английские значения в
|
||||||
|
manifest остаются fallback, а переводы metadata используют стабильные ключи:
|
||||||
|
|
||||||
|
```text
|
||||||
|
manifest.name
|
||||||
|
manifest.description
|
||||||
|
contributions.views.<id>.title
|
||||||
|
contributions.commands.<id>.title
|
||||||
|
contributions.statusBarItems.<id>.label
|
||||||
|
```
|
||||||
|
|
||||||
|
Внутренний UI плагина получает текущий язык только через публичный API:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const locale = api.i18n.getLocale();
|
||||||
|
const title = api.i18n.t('ui.title', undefined, 'Notes');
|
||||||
|
const unsubscribe = api.i18n.onDidChangeLocale(nextLocale => {
|
||||||
|
// Обновить текст без перемонтирования компонента и потери состояния.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Desktop поддерживает `system`, `en` и `ru`. В режиме `system` локали `ru-*`
|
||||||
|
выбирают русский язык, остальные — английский. Плагин отвечает за свои
|
||||||
|
переводы; desktop core не содержит тексты официальных плагинов.
|
||||||
|
|
||||||
## 4. Capabilities Instead Of Plugin Names
|
## 4. Capabilities Instead Of Plugin Names
|
||||||
|
|
||||||
Плагины не должны требовать конкретный плагин, если им нужна способность.
|
Плагины не должны требовать конкретный плагин, если им нужна способность.
|
||||||
|
|
@ -306,4 +342,3 @@ official.notes-0.1.0.vpkg
|
||||||
- checksums/signature later.
|
- checksums/signature later.
|
||||||
|
|
||||||
На первом этапе допустима ручная установка папкой в plugin directory.
|
На первом этапе допустима ручная установка папкой в plugin directory.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,9 @@ Implemented:
|
||||||
- browser inbox local receiver and minimal official Browser Inbox plugin;
|
- browser inbox local receiver and minimal official Browser Inbox plugin;
|
||||||
- sync server with device/user auth and operation push/pull;
|
- sync server with device/user auth and operation push/pull;
|
||||||
- SDK manifest/types/schema coverage for current plugin APIs;
|
- SDK manifest/types/schema coverage for current plugin APIs;
|
||||||
|
- persisted System/English/Russian application language selection, localized
|
||||||
|
desktop shell, public `api.i18n` plugin contract, and bilingual catalogs for
|
||||||
|
all official plugins;
|
||||||
- automated Go, frontend, official plugin, SDK, and real-sync smoke checks.
|
- automated Go, frontend, official plugin, SDK, and real-sync smoke checks.
|
||||||
|
|
||||||
Known remaining gaps:
|
Known remaining gaps:
|
||||||
|
|
@ -67,6 +70,8 @@ Known remaining gaps:
|
||||||
records conversions. Chunked large-file attachment capture remains future
|
records conversions. Chunked large-file attachment capture remains future
|
||||||
work.
|
work.
|
||||||
- Packaging/update/release workflow is not product-grade yet.
|
- Packaging/update/release workflow is not product-grade yet.
|
||||||
|
- Browser extension UI localization is not yet migrated to the shared
|
||||||
|
multilingual product policy.
|
||||||
|
|
||||||
## 4. Implementation Phases
|
## 4. Implementation Phases
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
Verstak Documentation
|
||||||
|
Copyright (C) 2026 Verstak contributors
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option) any
|
||||||
|
later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
details.
|
||||||
|
|
||||||
|
The complete license text is available at:
|
||||||
|
https://www.gnu.org/licenses/agpl-3.0.html
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
Сводные архитектурные артефакты платформы Верстак.
|
Сводные архитектурные артефакты платформы Верстак.
|
||||||
|
|
||||||
|
> Первый публичный выпуск — alpha. Инструкции для сборки и упаковки каждого
|
||||||
|
> исполняемого компонента находятся в его README: Desktop, official plugins,
|
||||||
|
> browser extension и SDK должны собираться из одной release-линейки.
|
||||||
|
|
||||||
## Содержание
|
## Содержание
|
||||||
|
|
||||||
- [Product Vision](01_Product_Vision.md) — что остаётся неизменным
|
- [Product Vision](01_Product_Vision.md) — что остаётся неизменным
|
||||||
|
|
@ -33,3 +37,8 @@
|
||||||
| `verstak-sync-server` | Сервер синхронизации |
|
| `verstak-sync-server` | Сервер синхронизации |
|
||||||
| `verstak-browser-extension` | Расширение браузера |
|
| `verstak-browser-extension` | Расширение браузера |
|
||||||
| `verstak-docs` | Документация |
|
| `verstak-docs` | Документация |
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Copyright © 2026 Verstak contributors. Документация распространяется на
|
||||||
|
условиях [GNU AGPLv3 или новее](LICENSE).
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,281 @@
|
||||||
|
# Platform Localization Implementation Plan
|
||||||
|
|
||||||
|
> **Execution note:** implement task-by-task in the current session. Do not use
|
||||||
|
> subagents. Use TDD for every production boundary and keep commits scoped to a
|
||||||
|
> single repository.
|
||||||
|
|
||||||
|
**Goal:** Add persisted System/Russian/English language selection to Verstak
|
||||||
|
Desktop, localize the shell and all official plugins, and establish the public
|
||||||
|
SDK contract used by future multilingual plugins.
|
||||||
|
|
||||||
|
**Architecture:** Desktop owns the application preference, locale resolution,
|
||||||
|
shell catalogs, safe plugin-catalog loading, and a generic runtime bridge. Each
|
||||||
|
plugin owns its manifest and UI catalogs and accesses them through
|
||||||
|
`api.i18n`. Manifest literals remain English fallbacks. Locale changes update
|
||||||
|
mounted UI without remounting plugin components.
|
||||||
|
|
||||||
|
**Tech stack:** Go/Wails backend tests, Svelte 4 and plain JavaScript frontend,
|
||||||
|
TypeScript SDK with Vitest, JSON Schema, browserless Node plugin smoke tests,
|
||||||
|
Playwright mocked-Wails E2E.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- `~/git/verstak` is UI reference only; do not copy its implementation.
|
||||||
|
- Core must not contain official plugin messages or depend on official plugin
|
||||||
|
IDs.
|
||||||
|
- Official and third-party plugins use the same manifest and runtime API.
|
||||||
|
- Store only `system`, `ru`, or `en`; default to `system`.
|
||||||
|
- Resolve `ru-*` system locales to `ru`, everything else to `en`.
|
||||||
|
- Do not remount plugins on locale changes or discard form/editor state.
|
||||||
|
- Preserve existing unrelated generated Wails binding changes in the desktop
|
||||||
|
worktree.
|
||||||
|
- Add tests before production changes and observe each focused test fail before
|
||||||
|
implementing its boundary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: SDK localization contract
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify `verstak-sdk/schemas/manifest.json`
|
||||||
|
- Modify `verstak-sdk/src/types.ts`
|
||||||
|
- Modify `verstak-sdk/src/plugin-api.ts`
|
||||||
|
- Modify `verstak-sdk/src/test-utils.ts`
|
||||||
|
- Modify `verstak-sdk/src/plugin-api.test.ts`
|
||||||
|
- Generated by build: `verstak-sdk/dist/*`
|
||||||
|
|
||||||
|
**Contract produced:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface PluginLocalizationConfig {
|
||||||
|
defaultLocale: string;
|
||||||
|
locales: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginI18nAPI {
|
||||||
|
getLocale(): 'ru' | 'en';
|
||||||
|
t(key: string, params?: Record<string, string | number>, fallback?: string): string;
|
||||||
|
onDidChangeLocale(listener: (locale: 'ru' | 'en') => void): Unsubscribe;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Add failing schema/type/mock tests for a valid manifest localization
|
||||||
|
block, required default catalog, safe relative paths, API shape, fallback,
|
||||||
|
interpolation, and locale-change notification.
|
||||||
|
- [ ] Run `npm test -- --run src/plugin-api.test.ts` and confirm RED.
|
||||||
|
- [ ] Add `localization` to the JSON Schema and TypeScript manifest types.
|
||||||
|
- [ ] Add `i18n` to `VerstakPluginAPI` and configurable locale/messages to
|
||||||
|
`createMockPluginAPI`.
|
||||||
|
- [ ] Run focused tests GREEN.
|
||||||
|
- [ ] Run `npm test`, `npm run lint`, and `npm run build`.
|
||||||
|
- [ ] Commit only SDK source/schema/generated dist changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Desktop application language setting
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify `verstak-desktop/internal/core/appsettings/manager.go`
|
||||||
|
- Modify `verstak-desktop/internal/core/appsettings/manager_test.go`
|
||||||
|
- Modify `verstak-desktop/internal/api/app.go`
|
||||||
|
- Modify `verstak-desktop/internal/api/app_test.go`
|
||||||
|
|
||||||
|
**Behavior:** missing language becomes `system`; valid updates persist; invalid
|
||||||
|
updates return an error; unrelated settings remain unchanged.
|
||||||
|
|
||||||
|
- [ ] Add failing manager tests for default, reload, and preservation.
|
||||||
|
- [ ] Add failing API tests for `GetAppSettings` and accepted/rejected patches.
|
||||||
|
- [ ] Run focused Go tests and confirm RED.
|
||||||
|
- [ ] Add `Language` to `appsettings.Config`, default/load normalization, and
|
||||||
|
update handling without changing unrelated values.
|
||||||
|
- [ ] Expose and validate `language` in the Wails API.
|
||||||
|
- [ ] Run focused tests GREEN.
|
||||||
|
- [ ] Run `gofmt` on changed Go files and `go test ./internal/core/appsettings
|
||||||
|
./internal/api`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Plugin localization metadata and safe catalog loading
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify `verstak-desktop/internal/core/plugin/plugin.go`
|
||||||
|
- Modify `verstak-desktop/internal/core/plugin/plugin_test.go`
|
||||||
|
- Modify `verstak-desktop/internal/api/app.go`
|
||||||
|
- Modify `verstak-desktop/internal/api/app_test.go`
|
||||||
|
- Generated Wails bindings are intentionally not regenerated over existing
|
||||||
|
user changes; frontend imports call the binding only after generation is
|
||||||
|
explicitly reconciled.
|
||||||
|
|
||||||
|
**Backend API:**
|
||||||
|
|
||||||
|
```go
|
||||||
|
GetPluginLocalization(pluginID, locale string) (map[string]string, string)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] Add failing manifest parsing tests for localization metadata.
|
||||||
|
- [ ] Add failing API tests for declared catalog reads, locale fallback input,
|
||||||
|
malformed JSON, non-string values, missing declarations, absolute paths,
|
||||||
|
backslashes, traversal, and containment.
|
||||||
|
- [ ] Run focused Go tests and confirm RED.
|
||||||
|
- [ ] Add localization manifest structs and the read-only safe catalog API.
|
||||||
|
- [ ] Run focused tests GREEN and `go test ./internal/core/plugin ./internal/api`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Frontend localization service
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Add `verstak-desktop/frontend/src/lib/i18n/catalogs/en.js`
|
||||||
|
- Add `verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js`
|
||||||
|
- Add `verstak-desktop/frontend/src/lib/i18n/index.js`
|
||||||
|
- Add `verstak-desktop/frontend/tests/i18n-test.mjs`
|
||||||
|
- Modify `verstak-desktop/frontend/src/main.js`
|
||||||
|
- Modify `verstak-desktop/frontend/src/lib/test/wails-mock.js`
|
||||||
|
|
||||||
|
- [ ] Add a failing standalone Node test for preference validation, system
|
||||||
|
locale resolution, interpolation, shell fallback, plugin catalog fallback,
|
||||||
|
subscription, and contribution-copy localization.
|
||||||
|
- [ ] Run `node frontend/tests/i18n-test.mjs` and confirm RED.
|
||||||
|
- [ ] Implement the framework-independent locale store and catalog cache.
|
||||||
|
- [ ] Initialize it from `GetAppSettings` before mounting Svelte.
|
||||||
|
- [ ] Extend the Wails mock with language state and plugin catalogs.
|
||||||
|
- [ ] Run the focused test GREEN and `npm run build`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Runtime `api.i18n` bridge
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify `verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js`
|
||||||
|
- Modify `verstak-desktop/frontend/src/lib/plugin-host/PluginBundleHost.svelte`
|
||||||
|
- Modify `verstak-desktop/frontend/tests/plugin-api-contributions-test.mjs`
|
||||||
|
- Modify `verstak-desktop/frontend/tests/bundle-host-test.cjs`
|
||||||
|
|
||||||
|
- [ ] Extend bridge tests first to require `getLocale`, `t`, and disposable
|
||||||
|
`onDidChangeLocale` behavior.
|
||||||
|
- [ ] Confirm focused RED.
|
||||||
|
- [ ] Preload the plugin catalog before mount and expose the i18n service
|
||||||
|
through the public API.
|
||||||
|
- [ ] Keep locale subscriptions under existing API disposal cleanup.
|
||||||
|
- [ ] Translate host loading/error states through shell catalogs.
|
||||||
|
- [ ] Confirm bridge/bundle tests GREEN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Language selector and shell migration
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify `verstak-desktop/frontend/src/App.svelte`
|
||||||
|
- Modify every Svelte file under `verstak-desktop/frontend/src/lib/shell/`
|
||||||
|
- Modify every Svelte file under
|
||||||
|
`verstak-desktop/frontend/src/lib/plugin-manager/`
|
||||||
|
- Modify `verstak-desktop/frontend/src/lib/plugin-host/PluginBundleHost.svelte`
|
||||||
|
- Modify `verstak-desktop/frontend/e2e/status-bar.spec.js`
|
||||||
|
- Add `verstak-desktop/frontend/e2e/localization.spec.js`
|
||||||
|
- Modify `verstak-desktop/frontend/tests/shell-source-contract-test.mjs` where
|
||||||
|
assertions intentionally depend on translated literals.
|
||||||
|
|
||||||
|
- [ ] Add failing E2E assertions for the language submenu/radio state,
|
||||||
|
persistence, Russian system locale, English fallback, and live shell update.
|
||||||
|
- [ ] Confirm focused Playwright RED.
|
||||||
|
- [ ] Add the language submenu to StatusBar and persist changes through
|
||||||
|
`UpdateAppSettings`.
|
||||||
|
- [ ] Replace shell user-facing literals with shell catalog keys, including
|
||||||
|
aria labels, tooltips, empty/loading/error/confirmation text.
|
||||||
|
- [ ] Localize copies of plugin manifests and contributions before display.
|
||||||
|
- [ ] Confirm focused E2E GREEN, standalone shell tests GREEN, and frontend
|
||||||
|
production build GREEN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Official plugin localization declarations and catalog checks
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify every `verstak-official-plugins/plugins/*/plugin.json`
|
||||||
|
- Add `locales/en.json` and `locales/ru.json` under every official plugin
|
||||||
|
- Add `verstak-official-plugins/scripts/check-locales.mjs`
|
||||||
|
- Modify `verstak-official-plugins/scripts/check.sh`
|
||||||
|
- Modify relevant plugin smoke harness API mocks under
|
||||||
|
`verstak-official-plugins/scripts/`
|
||||||
|
|
||||||
|
- [ ] Add the locale checker first and confirm it fails for missing metadata
|
||||||
|
and catalogs.
|
||||||
|
- [ ] Add localization declarations to all 13 manifests.
|
||||||
|
- [ ] Add English and Russian manifest/contribution keys to all catalogs.
|
||||||
|
- [ ] Verify catalog parity, string-only values, safe paths, and required
|
||||||
|
derived manifest keys.
|
||||||
|
- [ ] Run `./scripts/check.sh` GREEN before migrating runtime UI strings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Plain-JavaScript official plugin UI migration
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify `frontend/src/index.js` for activity, browser-inbox, default-editor,
|
||||||
|
file-preview, files, journal, notes, platform-test, search, secrets, todo, and
|
||||||
|
trash.
|
||||||
|
- Expand each plugin's `locales/en.json` and `locales/ru.json`.
|
||||||
|
- Modify focused `scripts/smoke-*-plugin.js` tests.
|
||||||
|
|
||||||
|
For each plugin, repeat independently:
|
||||||
|
|
||||||
|
- [ ] Add a failing bilingual rendering assertion and a locale-change state
|
||||||
|
preservation assertion to its existing smoke test.
|
||||||
|
- [ ] Add a local `t` helper backed only by `api.i18n`.
|
||||||
|
- [ ] Replace user-visible literals, attributes, empty/loading/error states,
|
||||||
|
confirmation prompts, and known validation messages with stable keys.
|
||||||
|
- [ ] Subscribe once at mount, update only rendered text/state, and unsubscribe
|
||||||
|
at unmount.
|
||||||
|
- [ ] Run the focused plugin smoke test GREEN before moving to the next plugin.
|
||||||
|
|
||||||
|
Do not translate IDs, paths, user data, logs, API labels in diagnostics, or
|
||||||
|
provider-returned values.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9: Svelte Sync plugin migration
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify `verstak-official-plugins/plugins/sync/frontend/src/SyncSettings.svelte`
|
||||||
|
- Modify `verstak-official-plugins/plugins/sync/frontend/src/SyncStatusBar.svelte`
|
||||||
|
- Modify `verstak-official-plugins/plugins/sync/frontend/src/index.js`
|
||||||
|
- Expand `verstak-official-plugins/plugins/sync/locales/en.json`
|
||||||
|
- Expand `verstak-official-plugins/plugins/sync/locales/ru.json`
|
||||||
|
- Modify Sync smoke/E2E tests as appropriate.
|
||||||
|
|
||||||
|
- [ ] Add failing English/Russian and live-change assertions.
|
||||||
|
- [ ] Bridge `api.i18n` into reactive Svelte state without remounting.
|
||||||
|
- [ ] Translate UI labels and known status/error chrome.
|
||||||
|
- [ ] Run Sync plugin build and focused tests GREEN.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10: Full verification and documentation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify `verstak-docs/04_Plugin_System.md`
|
||||||
|
- Modify `verstak-docs/07_Full_Implementation_Roadmap.md`
|
||||||
|
- Modify other contract docs only where the implemented behavior requires it.
|
||||||
|
|
||||||
|
- [ ] Run SDK: `npm test`, `npm run lint`, `npm run build`.
|
||||||
|
- [ ] Run desktop backend: `go test -count=1 ./...` and `go vet ./...`.
|
||||||
|
- [ ] Run desktop standalone frontend tests, production build, and focused then
|
||||||
|
full `npm run test:e2e`.
|
||||||
|
- [ ] Run official plugins: `./scripts/check.sh` and all plugin builds.
|
||||||
|
- [ ] Run `git diff --check` in every changed repository.
|
||||||
|
- [ ] Inspect changed-file lists and ensure pre-existing Wails generated changes
|
||||||
|
are not staged or overwritten.
|
||||||
|
- [ ] Update docs to match only verified behavior.
|
||||||
|
- [ ] Commit scoped changes in each repository.
|
||||||
|
- [ ] Report exact verification labels. If native Wails/WebKit was not exercised,
|
||||||
|
state: `GUI behavior was not verified in the real desktop shell.`
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
# Alpha Activity And Journal Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make Activity a durable, background-maintained session log that safely proposes Journal entries without creating a Дело automatically.
|
||||||
|
|
||||||
|
**Architecture:** Desktop adds a plugin background-service lifecycle, unsubscribe-capable event bridge, and append-only Activity log. The Activity plugin owns session reconstruction and watermarks; Journal receives an explicit reviewed candidate and requires a destination for unassigned sessions.
|
||||||
|
|
||||||
|
**Tech Stack:** Go, Wails, Svelte, plain JavaScript plugin bundles, Go tests, Node smoke tests, Playwright.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Activity raw events are retained for 60 days, 10,000 events, or 8 MiB, whichever is reached first.
|
||||||
|
- Raw activity uses append/compaction, not settings.json rewrites.
|
||||||
|
- Session scope is workspaceId plus path cache or unassigned.
|
||||||
|
- Point-event duration is capped at 10 minutes per adjacent pair; gap over 20 minutes starts another session.
|
||||||
|
- Accepted/dismissed watermarks consume only the reviewed slice.
|
||||||
|
- Journal save is always an explicit user action.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add background services, event unsubscription, and an append-only Activity log
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/plugin/plugin_test.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/events/bus.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/storage/api.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/storage/api_test.go
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/BackgroundPluginHost.svelte
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/App.svelte
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/types.ts
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/contributions.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/schemas/manifest.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-sdk/src/plugin-api.test.ts
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Adds contributes.backgroundServices with id and component fields.
|
||||||
|
- Bus Subscribe returns subscription ID; Unsubscribe removes only that handler.
|
||||||
|
- Adds ActivityLogAppend, ActivityLogRead, ActivityLogDeleteWorkspace, and ActivityLogCompact backend methods.
|
||||||
|
- Background bundle registration exposes start(api) returning a cleanup function.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing manifest, bus, and storage tests**
|
||||||
|
|
||||||
|
Add a manifest validation test and isolated bus-unsubscribe test:
|
||||||
|
|
||||||
|
~~~go
|
||||||
|
subA := bus.Subscribe("browser.activity.domain", handlerA)
|
||||||
|
subB := bus.Subscribe("browser.activity.domain", handlerB)
|
||||||
|
bus.Unsubscribe("browser.activity.domain", subA)
|
||||||
|
bus.Publish(events.Event{Name: "browser.activity.domain"})
|
||||||
|
if gotA != 0 || gotB != 1 { t.Fatalf("wrong selective unsubscribe: %d %d", gotA, gotB) }
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Add an append test asserting two appends create two NDJSON records and a
|
||||||
|
compaction test keeping only the newest record under a small test limit.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -run 'Test.*(Background|Unsubscribe|ActivityLog)' -count=1
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because subscriptions have no IDs and the log API does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the platform boundary**
|
||||||
|
|
||||||
|
Define:
|
||||||
|
|
||||||
|
~~~go
|
||||||
|
type ContributionBackgroundService struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Component string `json:"component"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityLogRecord struct {
|
||||||
|
ActivityID string `json:"activityId"`
|
||||||
|
Payload json.RawMessage `json:"payload"`
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Append NDJSON under plugin-data/verstak.activity/events.ndjson using a locked
|
||||||
|
append. Compact to the declared retention limits by rewriting a temporary file.
|
||||||
|
Keep only compact candidate state in plugin data. BackgroundPluginHost loads
|
||||||
|
enabled plugins with a background contribution once at app startup and calls the
|
||||||
|
registered start function; it calls cleanup on disable/reload/destroy.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/plugin ./internal/core/events ./internal/core/storage ./internal/api -count=1
|
||||||
|
git add internal/core/plugin internal/core/events internal/core/storage internal/api frontend/src/App.svelte frontend/src/lib/plugin-host
|
||||||
|
git commit -m "feat: add background activity runtime"
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-sdk add src/types.ts schemas/contributions.json schemas/manifest.json src/plugin-api.test.ts
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define plugin background services"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: focused tests prove one subscription, selective cleanup, append, and compaction.
|
||||||
|
|
||||||
|
### Task 2: Rebuild Activity sessions and persistent candidate watermarks
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/plugin.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/en.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/locales/ru.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-activity-plugin.js
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Activity background service registers ActivityService.start(api).
|
||||||
|
- Event record includes activityId, scope, occurredAt, durationSeconds, and sessionId.
|
||||||
|
- Candidate contains sessionId, handledThrough, estimatedMinutes, dateSlices, and source activities.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing smoke scenarios**
|
||||||
|
|
||||||
|
Add three explicit scenarios:
|
||||||
|
|
||||||
|
~~~js
|
||||||
|
const nineMinutes = eventsAt('2026-07-12T10:00:00Z', '2026-07-12T10:09:00Z');
|
||||||
|
assert.equal(buildSessions(nineMinutes)[0].estimatedMinutes, 9);
|
||||||
|
const late = appendLateEvent(existingSession, eventAt('2026-07-12T09:58:00Z'));
|
||||||
|
assert.equal(late.sessionId, existingSession.sessionId);
|
||||||
|
assert.equal(candidateAfterDismiss.sourceActivityIds.includes('a'), false);
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Also assert a 23:50 to 00:30 session has one session ID and date slices for both
|
||||||
|
local dates.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins
|
||||||
|
node scripts/smoke-activity-plugin.js
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because Activity derives candidate IDs from first and last events.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement background session state**
|
||||||
|
|
||||||
|
Replace view-mounted event recording with ActivityService.start(api). Persist
|
||||||
|
immutable generated session IDs and anchors in activity-state.json. Build scope
|
||||||
|
as either workspaceId plus root path or unassigned. Sum explicit browser
|
||||||
|
duration and only zero-duration adjacent point intervals, cap a point interval
|
||||||
|
at 10 minutes, split at 20 minutes, and cap a session at 120 minutes.
|
||||||
|
|
||||||
|
For accept or dismiss, store the ordered handledThrough watermark. New events
|
||||||
|
after the watermark require another 10 minutes before a new candidate appears;
|
||||||
|
late events at or before the watermark remain diagnostic only.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
node scripts/smoke-activity-plugin.js
|
||||||
|
./scripts/check.sh
|
||||||
|
git add plugins/activity scripts/smoke-activity-plugin.js
|
||||||
|
git commit -m "feat: persist activity sessions and watermarks"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: session, late-event, dismissal, browser-duration, and midnight smoke tests pass.
|
||||||
|
|
||||||
|
### Task 3: Review candidates safely in Journal and expose sessions in UI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/activity/frontend/src/index.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/frontend/src/index.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/en.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/journal/locales/ru.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-journal-plugin.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/activity.spec.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/todo.spec.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Journal candidate request accepts destinationWorkspaceId only for unassigned scope.
|
||||||
|
- Saved Journal entry stores sourceSessionId and handledThrough in addition to activity IDs.
|
||||||
|
- Activity UI has Sessions default and Events diagnostic view.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing E2E and smoke assertions**
|
||||||
|
|
||||||
|
Require an unassigned candidate to show a destination selector and reject Save
|
||||||
|
until an active workspace ID is selected:
|
||||||
|
|
||||||
|
~~~js
|
||||||
|
await page.getByRole('button', { name: /review/i }).click();
|
||||||
|
await expect(page.getByText(/choose.*дело/i)).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: /save/i })).toBeDisabled();
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Require the mock WritePluginSetting path to retain sourceSessionId after reload.
|
||||||
|
Require Clear Activity and Journal Delete to show cancellation-safe confirmation
|
||||||
|
before changing stored rows.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
|
||||||
|
npm run test:e2e -- activity.spec.js todo.spec.js
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because the current mock discards settings and Journal only accepts path-scoped candidates.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement review UI and local dates**
|
||||||
|
|
||||||
|
Render domain rows as hostname plus duration without URLs. Keep raw event
|
||||||
|
details in the Events tab. For unassigned scope, list active workspace IDs and
|
||||||
|
require a selection before handing the candidate to Journal. Preselect the
|
||||||
|
largest date slice, show both date slices, use local date conversion, and save
|
||||||
|
the session watermark with the Journal entry. Show a visible action if Journal
|
||||||
|
is disabled instead of silently dropping the request. Put a confirm/cancel
|
||||||
|
dialog in front of Clear Activity and Journal Delete; cancelling leaves the
|
||||||
|
append log, candidate state, and entry list untouched.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-activity-plugin.js && node scripts/smoke-journal-plugin.js
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- activity.spec.js todo.spec.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/activity plugins/journal scripts/smoke-activity-plugin.js scripts/smoke-journal-plugin.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: review activity sessions in journal"
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/activity.spec.js frontend/e2e/todo.spec.js frontend/src/lib/test/wails-mock.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover activity journal review flow"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: candidate-to-Journal, local date, and persisted mock workflows pass.
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
# Alpha Browser Domain Activity Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add explicitly consented, privacy-minimal active-tab domain timing to the browser extension and deliver immutable activity batches to Desktop.
|
||||||
|
|
||||||
|
**Architecture:** Manual captures remain unchanged. A new tracker stores mutable accumulators, immutable pending batches, and acknowledged IDs locally; Desktop accepts a separate authenticated activity endpoint and emits browser.activity.domain.
|
||||||
|
|
||||||
|
**Tech Stack:** Plain WebExtension JavaScript, Node tests, Go HTTP handler tests, shared JSON vectors.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Passive tracking defaults to disabled and requires explicit informed consent.
|
||||||
|
- Track only normalized hostname plus duration for an active tab in a focused window.
|
||||||
|
- Never send URL, title, text, page content, keystrokes, or history.
|
||||||
|
- A pending batch is byte-for-byte immutable; acknowledge only its own ID.
|
||||||
|
- Discard ambiguous or negative clock deltas and gaps over 10 minutes.
|
||||||
|
- Use hostname-normalization-v1 for extension, Desktop receiver, exclusions, and bindings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Define and test canonical hostname normalization
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname.js
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-hostname.js
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize.go
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/normalize_test.go
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces JavaScript normalizeHostnameV1(value, mode).
|
||||||
|
- Produces Go hostname.NormalizeV1(value string, mode Mode) (string, error).
|
||||||
|
- Mode is URLSource for HTTP(S) URLs and BareHost for binding/exclusion input.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the shared vector corpus and failing tests**
|
||||||
|
|
||||||
|
Include vectors for Unicode/punycode, trailing dot, port stripping/rejection,
|
||||||
|
IPv4, bracketed IPv6, localhost, one-label internal names, malformed labels,
|
||||||
|
and an overlong name:
|
||||||
|
|
||||||
|
~~~json
|
||||||
|
[
|
||||||
|
{"mode":"bare","input":"пример.рф.","want":"xn--e1afmkfd.xn--p1ai"},
|
||||||
|
{"mode":"url","input":"https://Example.COM:8443/a","want":"example.com"},
|
||||||
|
{"mode":"bare","input":"example.com:8443","error":"port"},
|
||||||
|
{"mode":"bare","input":"[::1]","want":"::1"}
|
||||||
|
]
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-browser-extension
|
||||||
|
node scripts/test-hostname.js
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: both fail because the normalizers do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement both normalizers**
|
||||||
|
|
||||||
|
Use the browser URL parser plus ASCII IDNA conversion in JavaScript. Use
|
||||||
|
net/url, net/netip, and golang.org/x/net/idna.Lookup.ToASCII in Go. Store
|
||||||
|
ASCII A-labels, lowercase names, no ports or IPv6 brackets, and reject malformed
|
||||||
|
or over-limit values.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-browser-extension && npm test
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/core/hostname -count=1
|
||||||
|
cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-browser-extension/shared/hostname-normalization-v1.json
|
||||||
|
cmp /home/mirivlad/git/verstak2/verstak-sdk/schemas/hostname-normalization-v1.json /home/mirivlad/git/verstak2/verstak-desktop/internal/core/hostname/testdata/hostname-normalization-v1.json
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-sdk add schemas/hostname-normalization-v1.json
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-sdk commit -m "feat: define hostname normalization vectors"
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-browser-extension add shared/hostname.js shared/hostname-normalization-v1.json scripts/test-hostname.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-browser-extension commit -m "feat: normalize activity hostnames"
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/hostname
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: normalize browser hostnames"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: every copied corpus passes in both implementations.
|
||||||
|
|
||||||
|
### Task 2: Add a separate authenticated Desktop activity receiver
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/browserreceiver/receiver_test.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Accepts POST /activities with BrowserDomainActivity.
|
||||||
|
- Emits browser.activity.domain with normalized hostname, duration, ID, and optional workspaceId.
|
||||||
|
- Returns status accepted and activityId for first delivery and duplicate delivery.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing handler tests**
|
||||||
|
|
||||||
|
Create a valid immutable request and then retry the same ID:
|
||||||
|
|
||||||
|
~~~go
|
||||||
|
payload := "{\"schemaVersion\":1,\"activityId\":\"a-1\",\"hostname\":\"пример.рф\",\"durationSeconds\":300,\"startedAt\":\"2026-07-12T10:00:00Z\",\"endedAt\":\"2026-07-12T10:05:00Z\"}"
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/activities", strings.NewReader(payload))
|
||||||
|
req.Header.Set("X-Verstak-Receiver-Token", "pair-token")
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Assert invalid tokens, URL-like hostnames, zero or oversize duration, and a
|
||||||
|
duplicate ID do not publish a second event.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver -run Activity -count=1
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because /activities is not routed.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the bounded idempotent route**
|
||||||
|
|
||||||
|
Add BrowserDomainActivity, a 30-day bounded receiver ID cache, and
|
||||||
|
handleActivity which invokes hostname.NormalizeV1. Do not reuse capture types
|
||||||
|
or persistence. In App, resolve an active exact hostname binding to workspaceId
|
||||||
|
and a path cache before Activity receives the event.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/browserreceiver ./internal/api -count=1
|
||||||
|
git add internal/core/browserreceiver/receiver.go internal/core/browserreceiver/receiver_test.go internal/api/app.go internal/api/app_test.go
|
||||||
|
git commit -m "feat: receive browser domain activity"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: authentication, normalization, idempotency, and binding tests pass.
|
||||||
|
|
||||||
|
### Task 3: Build the opt-in extension tracker and immutable delivery queue
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/activity-tracker.js
|
||||||
|
- Create: /home/mirivlad/git/verstak2/verstak-browser-extension/scripts/test-activity-tracker.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/api.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/background.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.html
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/popup/popup.css
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/en.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/shared/locales/ru.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/chromium/manifest.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-browser-extension/firefox/manifest.json
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Persists activeAccumulator, pendingBatches, and acknowledgedIds under verstak.activityTracker.
|
||||||
|
- Produces sendActivity(receiverUrl, token, immutablePayload).
|
||||||
|
- Settings include passiveActivityEnabled false and excludedDomains empty.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing state-machine tests**
|
||||||
|
|
||||||
|
Cover consent, active-tab-only timing, long-gap discard, and the A/B batch case:
|
||||||
|
|
||||||
|
~~~js
|
||||||
|
tracker.addElapsed('example.com', 600, start, end);
|
||||||
|
const batchA = tracker.freeze('example.com');
|
||||||
|
tracker.addElapsed('example.com', 300, laterStart, laterEnd);
|
||||||
|
assert.equal(tracker.pendingBatches[0].payload.durationSeconds, 600);
|
||||||
|
tracker.acknowledge(batchA.activityId);
|
||||||
|
assert.equal(tracker.activeAccumulator['example.com'].durationSeconds, 300);
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-browser-extension
|
||||||
|
node scripts/test-activity-tracker.js
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because the state machine is absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement tracker, disclosure, and listeners**
|
||||||
|
|
||||||
|
Implement the tracker as pure functions. In background.js, register
|
||||||
|
tabs.onActivated, tabs.onUpdated, windows.onFocusChanged, idle.onStateChanged,
|
||||||
|
and a five-minute alarm only while opted in. Set idle detection to 600 seconds.
|
||||||
|
On a gap over 600 seconds, clock rollback, lock, or browser startup, reset the
|
||||||
|
active checkpoint without adding time. Retry frozen batches oldest-first and
|
||||||
|
remove only matching IDs.
|
||||||
|
|
||||||
|
Show an unchecked Russian/English consent explanation in settings. Add windows,
|
||||||
|
alarms, and idle permissions to both manifests. Keep manual capture queue and
|
||||||
|
popup actions unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
npm test
|
||||||
|
node scripts/test-activity-tracker.js
|
||||||
|
git add shared chromium/manifest.json firefox/manifest.json scripts/test-activity-tracker.js
|
||||||
|
git commit -m "feat: add opt-in browser domain tracker"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: tracker unit tests and existing extension tests pass.
|
||||||
|
|
@ -0,0 +1,229 @@
|
||||||
|
# Alpha Browser Inbox Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Preserve a manually captured link when it leaves the global Inbox, give Browser Inbox a usable archive, and make links durable across case lifecycle changes.
|
||||||
|
|
||||||
|
**Architecture:** A capture remains one canonical record. Its global archive state is independent of its workspaceId relation; domain bindings use the same stable identity. Browser Inbox owns capture UI and migration, while Desktop exposes a narrow URL-open capability for user-initiated links.
|
||||||
|
|
||||||
|
**Tech Stack:** Plain JavaScript plugin bundle, Go Wails API, Node smoke tests, Playwright.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Only manual extension actions create Browser Inbox captures.
|
||||||
|
- Captures have at most one optional case assignment in this alpha.
|
||||||
|
- Remove from global Inbox archives; only Delete everywhere is permanent.
|
||||||
|
- Assignment and binding use workspaceId, never path as identity.
|
||||||
|
- Create .url files through files.write and never overwrite a name silently.
|
||||||
|
- URL opening is direct HTTP(S) opening, not a Linux .url association.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Migrate Browser Inbox records and implement archive state
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/en.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/locales/ru.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Canonical capture fields: globalState, workspaceId, workspaceState, workspaceRootPath, workspaceTrashId.
|
||||||
|
- Domain binding value: workspaceId plus workspaceRootPath cache plus state.
|
||||||
|
- Mutations: archive, restore, assign, unassign, deleteEverywhere, migrate.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing migration and archive smoke tests**
|
||||||
|
|
||||||
|
Use a legacy path assignment and assert it resolves to a stable ID. Test the
|
||||||
|
original data-loss regression:
|
||||||
|
|
||||||
|
~~~js
|
||||||
|
await inbox.assignWorkspace('capture-1', activeWorkspace);
|
||||||
|
await inbox.archiveCapture('capture-1');
|
||||||
|
assert.equal(globalRows().some(row => row.captureId === 'capture-1'), false);
|
||||||
|
assert.equal(workspaceRows(activeWorkspace.workspaceId).some(row => row.captureId === 'capture-1'), true);
|
||||||
|
await inbox.restoreCapture('capture-1');
|
||||||
|
assert.equal(globalRows().some(row => row.captureId === 'capture-1'), true);
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins
|
||||||
|
node scripts/smoke-browser-inbox-plugin.js
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because archive and assignment are currently one mutable path field.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement canonical state and idempotent migration**
|
||||||
|
|
||||||
|
Normalize every capture on read. Set legacy captures globalState active. Resolve
|
||||||
|
legacy paths through Desktop identities: active marker gives workspaceId,
|
||||||
|
otherwise set workspaceState unavailable. Store global captures once; workspace
|
||||||
|
views filter the canonical collection by workspaceId rather than duplicate
|
||||||
|
storage keys. Reject permanent deletion without an explicit confirm token from
|
||||||
|
the UI.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
node scripts/smoke-browser-inbox-plugin.js
|
||||||
|
./scripts/check.sh
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: preserve archived browser inbox captures"
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/api/app.go internal/api/app_test.go
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: migrate browser inbox relations"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: migration, archive, restore, and assignment tests pass.
|
||||||
|
|
||||||
|
### Task 2: Handle rename, Trash, restore, purge, and archive UI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/browser-inbox.spec.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Receives workspace.renamed, workspace.trashed, workspace.restored, and workspace.purged.
|
||||||
|
- Status filter values are active, archive, all.
|
||||||
|
- Archive bulk action is restore; bulk permanent delete is not offered.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing lifecycle and UI tests**
|
||||||
|
|
||||||
|
Assert a rename changes only path cache, trash disables routing, restore restores
|
||||||
|
the same workspace ID, purge makes captures unassigned and bindings orphaned:
|
||||||
|
|
||||||
|
~~~js
|
||||||
|
await emitWorkspaceEvent('workspace.trashed', { workspaceId: 'w-1', trashId: 't-1' });
|
||||||
|
assert.equal(capture.workspaceState, 'trashed');
|
||||||
|
assert.equal(domainBinding('client.example').state, 'trashed');
|
||||||
|
await emitWorkspaceEvent('workspace.purged', { workspaceId: 'w-1', trashId: 't-1' });
|
||||||
|
assert.equal(capture.workspaceState, 'unassigned');
|
||||||
|
assert.equal(domainBinding('client.example').state, 'orphaned');
|
||||||
|
~~~
|
||||||
|
|
||||||
|
In Playwright, select Archive, restore one visible item, and bulk-restore only
|
||||||
|
the filtered archive rows. Also click Delete everywhere for an assigned capture,
|
||||||
|
assert the dialog names its assigned Дело, cancel, and assert that the global
|
||||||
|
and assigned rows still exist.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
|
||||||
|
npm run test:e2e -- browser-inbox.spec.js
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because the current UI has only processed filters and destructive Clear.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement lifecycle and labels**
|
||||||
|
|
||||||
|
Subscribe once from the Browser Inbox background service. Update path caches on
|
||||||
|
rename only when workspaceId matches. On external unavailable, do not route to
|
||||||
|
the same path again. Add Active, Archive, and All filters; make search apply
|
||||||
|
inside the selected filter; show Archive badge inside a case; add Restore to
|
||||||
|
Inbox and visible filtered bulk restore. Rename Clear to an archive action with
|
||||||
|
count confirmation. Keep Delete everywhere separate and require a dialog that
|
||||||
|
names the assigned Дело; cancellation must perform no mutation.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend && npm run test:e2e -- browser-inbox.spec.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: add browser inbox archive lifecycle"
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop add frontend/e2e/browser-inbox.spec.js frontend/src/lib/test/wails-mock.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "test: cover browser inbox archive lifecycle"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: archive filters, restore, rename, Trash, restore, and purge tests pass.
|
||||||
|
|
||||||
|
### Task 3: Save and open durable links without file-association reliance
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/permissions/registry.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-host/VerstakPluginAPI.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/plugin.json
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/plugins/browser-inbox/frontend/src/index.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-official-plugins/scripts/smoke-browser-inbox-plugin.js
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Permission and API: urls.openExternal(url string) for user-initiated HTTP(S) URLs only.
|
||||||
|
- SaveLinkInCase(captureId, workspaceId, filename) creates Links/name.url with InternetShortcut URL field.
|
||||||
|
- Collision response offers a proposed unique filename and never overwrites.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing API and plugin tests**
|
||||||
|
|
||||||
|
Add tests for invalid URL, readonly failure, unique filename, collision, and
|
||||||
|
direct opener argument:
|
||||||
|
|
||||||
|
~~~go
|
||||||
|
if errStr := app.OpenExternalURL("browser.plugin", "https://example.test/a"); errStr != "" {
|
||||||
|
t.Fatal(errStr)
|
||||||
|
}
|
||||||
|
if got := opener.Arguments[0]; got != "https://example.test/a" {
|
||||||
|
t.Fatalf("opened %q, want URL not .url path", got)
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Add plugin smoke assertions for a title-derived name, Link fallback, and
|
||||||
|
proposal Link (2).url after a collision.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'Test.*OpenExternalURL' -count=1
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins
|
||||||
|
node scripts/smoke-browser-inbox-plugin.js
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because only file-path external opening exists.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement safe save and open**
|
||||||
|
|
||||||
|
Register urls.openExternal as dangerous. Validate HTTP(S), invoke the existing
|
||||||
|
OS opener with the URL string, and expose api.urls.openExternal. Browser Inbox
|
||||||
|
requests that permission, creates Links through files.createFolder and
|
||||||
|
files.writeText using:
|
||||||
|
|
||||||
|
~~~text
|
||||||
|
[InternetShortcut]
|
||||||
|
URL=https://example.test/
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Sanitize a 96-character filename stem. On collision show an editable dialog
|
||||||
|
with proposed name (2).url and Cancel; leave capture state unchanged on errors.
|
||||||
|
When opening a saved link, parse and validate URL= before invoking the URL API.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop && GOCACHE=/tmp/verstak-go-cache go test ./internal/api -count=1
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-official-plugins && node scripts/smoke-browser-inbox-plugin.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop add internal/core/permissions internal/api frontend/src/lib/plugin-host
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-desktop commit -m "feat: open external URLs safely"
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins add plugins/browser-inbox scripts/smoke-browser-inbox-plugin.js
|
||||||
|
git -C /home/mirivlad/git/verstak2/verstak-official-plugins commit -m "feat: save browser inbox links safely"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: URL API tests and link save/collision smoke tests pass.
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
# Alpha Shell UX Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Make the Desktop overview scoped and actionable, hide diagnostic plugin IDs by default, and remove alpha UI strings that expose implementation noise.
|
||||||
|
|
||||||
|
**Architecture:** The shell consumes stable workspace IDs and plugin capabilities rather than hard-coded namespaces. Overview builds deterministic case-scoped projections; debug display is a presentation preference plus a session-only command-line override.
|
||||||
|
|
||||||
|
**Tech Stack:** Svelte, Wails app settings, Go tests, Playwright E2E, existing i18n catalogs.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Default UI labels are Russian and use Дела, Входящие, Активности, Журнал.
|
||||||
|
- A normal tab never shows a plugin ID.
|
||||||
|
- Settings Debug Show plugin IDs defaults false; Desktop --debug enables it only for that run.
|
||||||
|
- Overview never leaks an unscoped event into every case.
|
||||||
|
- Todo entries are included only if todo.workspace capability is loaded.
|
||||||
|
- No action opens or creates a Дело implicitly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add effective debug display setting and human plugin tab labels
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/main.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/appsettings/manager_test.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/WorkspaceHost.svelte
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/plugin-manager/PluginManager.svelte
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/plugin-api-bridge.spec.js
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Persisted app setting showPluginIds bool defaults false.
|
||||||
|
- GetAppSettings returns effectiveShowPluginIds and UpdateAppSettings accepts showPluginIds.
|
||||||
|
- Runtime --debug sets effectiveShowPluginIds true without persisting it.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing app-setting and E2E tests**
|
||||||
|
|
||||||
|
Add a Go test proving the setting preserves theme and language, plus a browser
|
||||||
|
test proving normal mode has no raw ID and debug mode has one:
|
||||||
|
|
||||||
|
~~~js
|
||||||
|
await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.' })).toHaveCount(0);
|
||||||
|
await mockAppSettings({ showPluginIds: true });
|
||||||
|
await expect(page.locator('[role="tab"]').filter({ hasText: 'verstak.default-editor' })).toHaveCount(1);
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -run 'Test.*PluginIds' -count=1
|
||||||
|
cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because no display preference exists.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement presentation-only debug state**
|
||||||
|
|
||||||
|
Parse --debug before Wails startup and keep it in App runtime state. Merge it
|
||||||
|
with the persisted bool when returning effective settings. Render the localized
|
||||||
|
manifest title normally; append the plugin ID only when effectiveShowPluginIds
|
||||||
|
is true. Add the Debug settings checkbox and localized explanatory copy.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/appsettings ./internal/api -count=1
|
||||||
|
cd frontend && npm run test:e2e -- plugin-api-bridge.spec.js
|
||||||
|
git add main.go internal/core/appsettings internal/api frontend/src frontend/e2e/plugin-api-bridge.spec.js
|
||||||
|
git commit -m "feat: hide plugin IDs outside debug mode"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: normal and debug labels are both verified.
|
||||||
|
|
||||||
|
### Task 2: Implement deterministic case-scoped Overview
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/TodaySurface.svelte
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/shell/WorkspaceHost.svelte
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/en.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/i18n/catalogs/ru.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-today.spec.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-p0.spec.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/lib/test/wails-mock.js
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Every overview row has workspaceId and lastMeaningfulAt.
|
||||||
|
- Needs attention maximum is five; Continue work maximum is four; Recent changes maximum is eight.
|
||||||
|
- Hidden recommendation state is keyed by entity ID and invalidated by later meaningful change.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing scope and ranking tests**
|
||||||
|
|
||||||
|
Add fixture events for two workspace IDs plus an unscoped event:
|
||||||
|
|
||||||
|
~~~js
|
||||||
|
await seedOverview({
|
||||||
|
selectedWorkspaceId: 'client-a',
|
||||||
|
events: [
|
||||||
|
{ activityId: 'a', workspaceId: 'client-a', type: 'note.saved', occurredAt: now },
|
||||||
|
{ activityId: 'b', workspaceId: 'client-b', type: 'file.changed', occurredAt: now },
|
||||||
|
{ activityId: 'global', type: 'workspace.selected', occurredAt: now }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
await expect(page.getByText(/client-b/i)).toHaveCount(0);
|
||||||
|
await expect(page.locator('[data-overview-section="continue"] [data-overview-item]')).toHaveCount(4);
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Also assert no selection shows only the select/create prompt and up to five
|
||||||
|
unassigned active Inbox captures.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop/frontend
|
||||||
|
npm run test:e2e -- ux-today.spec.js ux-p0.spec.js
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because rowsFor currently accepts untagged events for every workspace.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement exact overview projections**
|
||||||
|
|
||||||
|
Replace path-only filtering with workspaceId equality. Build:
|
||||||
|
|
||||||
|
1. Needs attention: ready Journal candidates, active unprocessed captures, then
|
||||||
|
urgent Todo rows only if todo.workspace capability exists; take five.
|
||||||
|
2. Continue work: distinct entities updated in 14 days; sort by
|
||||||
|
lastMeaningfulAt descending and the documented type tie-break; take four.
|
||||||
|
3. Recent changes: note save/create, file create/rename or last change per
|
||||||
|
file, saved link, and Journal create in seven days; take eight.
|
||||||
|
|
||||||
|
Persist Hide recommendation without changing underlying data. Use localized
|
||||||
|
empty states and remove English technical strings.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
npm run test:e2e -- ux-today.spec.js ux-p0.spec.js
|
||||||
|
git add src/lib/shell/TodaySurface.svelte src/lib/shell/WorkspaceHost.svelte src/lib/i18n frontend/e2e/ux-today.spec.js frontend/e2e/ux-p0.spec.js src/lib/test/wails-mock.js
|
||||||
|
git commit -m "feat: make overview case-scoped and actionable"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: limits, scope, empty state, and recommendation hide tests pass.
|
||||||
|
|
||||||
|
### Task 3: Verify alpha UX as a user sees it
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/e2e/ux-followup.spec.js
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/scripts/check-gui.sh
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- E2E fixture uses effectiveShowPluginIds false by default.
|
||||||
|
- GUI checker accepts a --debug-labels case.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the final failing alpha scenarios**
|
||||||
|
|
||||||
|
Add a scenario that creates a capture, assigns it, archives it globally, opens
|
||||||
|
the assigned case, and verifies the capture is still present. Add a session
|
||||||
|
candidate path to Journal and assert normal tab labels are human-readable.
|
||||||
|
|
||||||
|
~~~js
|
||||||
|
await archiveGlobalCapture('capture-1');
|
||||||
|
await openWorkspace('client-a');
|
||||||
|
await expect(page.getByTestId('browser-capture-capture-1')).toBeVisible();
|
||||||
|
await expect(page.getByRole('tab', { name: /verstak\./i })).toHaveCount(0);
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
npm run test:e2e -- ux-followup.spec.js
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL until the prior plans are integrated.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add deterministic GUI smoke commands**
|
||||||
|
|
||||||
|
Extend check-gui.sh to start an isolated test vault, run normal and --debug
|
||||||
|
screens, and save screenshots under frontend/e2e-results. The script must not
|
||||||
|
read or mutate the user's normal vault.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
npm run test:e2e -- ux-followup.spec.js
|
||||||
|
GOCACHE=/tmp/verstak-go-cache ./scripts/check-gui.sh
|
||||||
|
git add frontend/e2e/ux-followup.spec.js scripts/check-gui.sh
|
||||||
|
git commit -m "test: cover first alpha UX smoke flow"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: E2E and isolated GUI smoke pass.
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
# Alpha Workspace Identity Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Give every managed Дело a durable UUID identity and publish lifecycle events that let alpha features follow a case across rename, Trash, restore, and path reuse.
|
||||||
|
|
||||||
|
**Architecture:** The filesystem remains the source of truth for whether a case folder exists. A UUID marker inside each case is the relation identity; Desktop metadata indexes that marker and paths remain presentation caches.
|
||||||
|
|
||||||
|
**Tech Stack:** Go, Wails bindings, JSON metadata, Go tests.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- A new Дело is created only by an explicit user action.
|
||||||
|
- workspaceId is UUID v4; a path is never relation identity.
|
||||||
|
- The marker is .verstak/workspace.json inside the case.
|
||||||
|
- Existing writable cases migrate without data loss; non-writable cases are view-only.
|
||||||
|
- Use TDD and make one commit per task.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Store and resolve durable workspace identities
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces Workspace.ID string and Metadata.WorkspaceID string.
|
||||||
|
- Produces func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error).
|
||||||
|
- Produces func (m *Manager) ListWorkspaceIdentities() ([]WorkspaceIdentity, error).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing identity tests**
|
||||||
|
|
||||||
|
Add tests proving creation writes a UUID marker, rename retains it, and a newly
|
||||||
|
created folder after external removal gets another UUID:
|
||||||
|
|
||||||
|
~~~go
|
||||||
|
ws, err := m.CreateWorkspace("Clients", "default")
|
||||||
|
if err != nil { t.Fatal(err) }
|
||||||
|
first, err := m.EnsureWorkspaceIdentity(ws.Name)
|
||||||
|
if err != nil || first.WorkspaceID == "" { t.Fatalf("identity = %+v, %v", first, err) }
|
||||||
|
if err := m.RenameWorkspace("Clients", "Clients-2026"); err != nil { t.Fatal(err) }
|
||||||
|
renamed, _ := m.EnsureWorkspaceIdentity("Clients-2026")
|
||||||
|
if renamed.WorkspaceID != first.WorkspaceID { t.Fatal("rename changed workspace ID") }
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused test to verify it fails**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
cd /home/mirivlad/git/verstak2/verstak-desktop
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -run 'Test.*Workspace.*Identity' -count=1
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because WorkspaceIdentity and EnsureWorkspaceIdentity do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write the minimal marker implementation**
|
||||||
|
|
||||||
|
Add these types and use them in CreateWorkspace, GetWorkspaceMetadata, and
|
||||||
|
workspace listing:
|
||||||
|
|
||||||
|
~~~go
|
||||||
|
type WorkspaceIdentity struct {
|
||||||
|
WorkspaceID string `json:"workspaceId"`
|
||||||
|
RootPath string `json:"rootPath"`
|
||||||
|
State string `json:"state"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceIdentityRelativePath = ".verstak/workspace.json"
|
||||||
|
|
||||||
|
func (m *Manager) EnsureWorkspaceIdentity(name string) (WorkspaceIdentity, error) {
|
||||||
|
// Validate the folder, read the in-folder marker, and atomically create
|
||||||
|
// a UUID-v4 marker only when absent and writable.
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Write the same ID to central metadata only as an index. Never recover an ID from
|
||||||
|
stale path-keyed metadata when the in-folder marker is absent.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace -count=1
|
||||||
|
git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go
|
||||||
|
git commit -m "feat: add durable workspace identities"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: tests pass and the commit contains only workspace identity changes.
|
||||||
|
|
||||||
|
### Task 2: Expose identity and lifecycle events through Desktop
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/src/App.svelte
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/api/App.d.ts
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/frontend/wailsjs/go/models.ts
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces ListWorkspaceIdentities() ([]workspace.WorkspaceIdentity, string).
|
||||||
|
- Lifecycle payloads include workspaceId and workspaceRootPath; Trash also includes trashId.
|
||||||
|
- Produces workspace.restored and workspace.purged events.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add failing API tests**
|
||||||
|
|
||||||
|
Extend TestWorkspaceAPIPublishesLifecycleEvents:
|
||||||
|
|
||||||
|
~~~go
|
||||||
|
if got := received["workspace.created"]["workspaceId"]; got == "" {
|
||||||
|
t.Fatal("workspace.created must include workspaceId")
|
||||||
|
}
|
||||||
|
if got := received["workspace.renamed"]["workspaceId"]; got != createdID {
|
||||||
|
t.Fatalf("rename ID = %v, want %s", got, createdID)
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Add restore and purge tests around RestoreVaultTrash and DeleteVaultTrash that
|
||||||
|
assert the original UUID and trash ID are emitted.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/api -run 'TestWorkspaceAPI.*Lifecycle' -count=1
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because lifecycle payloads only identify a path.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Publish complete identity lifecycle**
|
||||||
|
|
||||||
|
Resolve the marker before publishing create, rename, selected, and trashed
|
||||||
|
events. Detect workspace Trash metadata in RestoreVaultTrash and
|
||||||
|
DeleteVaultTrash, then publish:
|
||||||
|
|
||||||
|
~~~go
|
||||||
|
map[string]interface{}{
|
||||||
|
"operation": "restore",
|
||||||
|
"workspaceId": identity.WorkspaceID,
|
||||||
|
"workspaceRootPath": restoredRoot,
|
||||||
|
"trashId": trashID,
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Change App.svelte workspace-node conversion to preserve workspace.id while using
|
||||||
|
rootPath only for selection and display.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/api ./internal/core/workspace -count=1
|
||||||
|
git add internal/api/app.go internal/api/app_test.go frontend/src/App.svelte frontend/wailsjs/go/api/App.d.ts frontend/wailsjs/go/models.ts
|
||||||
|
git commit -m "feat: publish workspace identity lifecycle"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: focused API and workspace tests pass.
|
||||||
|
|
||||||
|
### Task 3: Migrate legacy references and repair duplicate IDs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/core/workspace/manager_test.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app.go
|
||||||
|
- Modify: /home/mirivlad/git/verstak2/verstak-desktop/internal/api/app_test.go
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces RepairWorkspaceIdentity(keepRootPath, regenerateRootPath string) string.
|
||||||
|
- Identity states are active, unavailable, or duplicate.
|
||||||
|
- Later Inbox and Activity plans consume workspaceId through ListWorkspaceIdentities.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing duplicate and legacy tests**
|
||||||
|
|
||||||
|
Create two folders with copied marker data and assert neither is an active
|
||||||
|
relation target. Create a legacy path-keyed capture fixture and assert a
|
||||||
|
resolved path gets an ID while a missing path becomes unavailable:
|
||||||
|
|
||||||
|
~~~go
|
||||||
|
if errStr := app.RepairWorkspaceIdentity("Original", "Copied"); errStr != "" {
|
||||||
|
t.Fatal(errStr)
|
||||||
|
}
|
||||||
|
if originalID == copiedID { t.Fatal("repair must issue a new ID") }
|
||||||
|
~~~
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -run 'Test.*(Duplicate|Legacy).*Workspace' -count=1
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: FAIL because copied markers are accepted as ordinary folders.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement deterministic repair**
|
||||||
|
|
||||||
|
Group active roots by marker UUID during identity listing. Mark a group with
|
||||||
|
more than one root duplicate and exclude it from assignment targets.
|
||||||
|
RepairWorkspaceIdentity verifies the duplicate, keeps the first marker, creates
|
||||||
|
a new marker for the second root, and moves no relation data.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
~~~bash
|
||||||
|
GOCACHE=/tmp/verstak-go-cache go test ./internal/core/workspace ./internal/api -count=1
|
||||||
|
git add internal/core/workspace/manager.go internal/core/workspace/manager_test.go internal/api/app.go internal/api/app_test.go
|
||||||
|
git commit -m "feat: repair duplicate workspace identities"
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Expected: legacy, path-reuse, and duplicate-ID tests pass.
|
||||||
|
|
@ -0,0 +1,384 @@
|
||||||
|
# Platform Localization Design
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Verstak v2 currently renders most desktop shell and official plugin text in
|
||||||
|
English. Application settings do not store a language, the frontend plugin API
|
||||||
|
has no localization contract, and plugin manifests expose only literal English
|
||||||
|
metadata. The sync server already has separate Russian and English catalogs and
|
||||||
|
is outside the scope of this desktop milestone. The browser extension also has
|
||||||
|
its own lifecycle and is explicitly deferred to a later milestone.
|
||||||
|
|
||||||
|
The first localization milestone covers:
|
||||||
|
|
||||||
|
- the `verstak-desktop` shell and core-owned screens;
|
||||||
|
- every plugin in `verstak-official-plugins`, including `platform-test`;
|
||||||
|
- the public SDK and manifest contract required by future third-party plugins;
|
||||||
|
- Russian and English;
|
||||||
|
- a persisted language selector in the desktop settings menu.
|
||||||
|
|
||||||
|
The existing `~/git/verstak` project remains a visual reference only. Its code
|
||||||
|
and localization architecture are not inputs to this implementation.
|
||||||
|
|
||||||
|
## Product Decisions
|
||||||
|
|
||||||
|
- The stored language preference is `system`, `ru`, or `en`.
|
||||||
|
- A new installation defaults to `system`.
|
||||||
|
- System locales beginning with `ru` resolve to Russian; all other system
|
||||||
|
locales resolve to English.
|
||||||
|
- The settings menu offers System, Russian, and English choices.
|
||||||
|
- Changing the preference updates the running UI without restarting the app.
|
||||||
|
- The preference is installation-local and is not stored in or synchronized
|
||||||
|
with a vault.
|
||||||
|
- Browser extension localization is a separate follow-up.
|
||||||
|
- Sync server localization remains independent from the desktop preference.
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Recommended: platform contract with plugin-owned catalogs
|
||||||
|
|
||||||
|
The shell owns shell translations. Each plugin owns its catalogs and declares
|
||||||
|
them in its manifest. The public plugin API exposes the resolved locale,
|
||||||
|
translation, and locale-change subscription.
|
||||||
|
|
||||||
|
Trade-offs:
|
||||||
|
|
||||||
|
- preserves repository and plugin ownership boundaries;
|
||||||
|
- gives third-party plugins the same mechanism as official plugins;
|
||||||
|
- requires coordinated SDK, desktop, manifest, and plugin changes;
|
||||||
|
- avoids teaching desktop core the UI vocabulary of official plugins.
|
||||||
|
|
||||||
|
### One desktop-wide catalog
|
||||||
|
|
||||||
|
Desktop would contain translations for shell and all official plugins.
|
||||||
|
|
||||||
|
Trade-offs:
|
||||||
|
|
||||||
|
- initially faster;
|
||||||
|
- couples core releases to plugin text and IDs;
|
||||||
|
- prevents independently distributed plugins from owning their localization;
|
||||||
|
- contradicts the dynamic plugin architecture.
|
||||||
|
|
||||||
|
### Independent dictionaries without a platform API
|
||||||
|
|
||||||
|
Every plugin would read a global value or DOM event and implement translation
|
||||||
|
on its own.
|
||||||
|
|
||||||
|
Trade-offs:
|
||||||
|
|
||||||
|
- small initial runtime change;
|
||||||
|
- inconsistent fallback and interpolation behavior;
|
||||||
|
- no stable SDK contract for third-party plugins;
|
||||||
|
- makes live locale changes and manifest metadata unreliable.
|
||||||
|
|
||||||
|
## Chosen Architecture
|
||||||
|
|
||||||
|
Use the platform contract with plugin-owned catalogs.
|
||||||
|
|
||||||
|
Localization is a UI platform service, not a user feature plugin. Desktop owns
|
||||||
|
language preference resolution, catalog loading, shell translation, and the
|
||||||
|
generic runtime bridge. It does not own plugin message content.
|
||||||
|
|
||||||
|
The data flow is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app config language preference
|
||||||
|
|
|
||||||
|
v
|
||||||
|
desktop locale store -----> shell catalogs
|
||||||
|
|
|
||||||
|
+------------------> localized manifest contributions
|
||||||
|
|
|
||||||
|
+------------------> api.i18n for each plugin
|
||||||
|
|
|
||||||
|
v
|
||||||
|
plugin-owned catalogs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Application Setting
|
||||||
|
|
||||||
|
Add `Language string` to the desktop application config and expose it as
|
||||||
|
`language` through `GetAppSettings` and `UpdateAppSettings`.
|
||||||
|
|
||||||
|
Accepted persisted values:
|
||||||
|
|
||||||
|
```text
|
||||||
|
system
|
||||||
|
ru
|
||||||
|
en
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- missing or empty values load as `system`;
|
||||||
|
- unknown values are rejected by the update API and never persisted;
|
||||||
|
- an existing config requires no schema migration beyond applying the missing
|
||||||
|
default;
|
||||||
|
- updating language must not reset `devMode`, theme, workbench preferences, or
|
||||||
|
other unrelated settings;
|
||||||
|
- `system` is resolved in the frontend from `navigator.languages` or
|
||||||
|
`navigator.language`, with `en` as the safe fallback.
|
||||||
|
|
||||||
|
The settings menu renders language names in their own language so the user can
|
||||||
|
recover from an accidental switch:
|
||||||
|
|
||||||
|
```text
|
||||||
|
System / Системный
|
||||||
|
English
|
||||||
|
Русский
|
||||||
|
```
|
||||||
|
|
||||||
|
The active option is visibly marked and uses menu radio semantics.
|
||||||
|
|
||||||
|
## Desktop Localization Service
|
||||||
|
|
||||||
|
Add one small frontend localization module that owns:
|
||||||
|
|
||||||
|
- the stored preference;
|
||||||
|
- the resolved locale (`ru` or `en`);
|
||||||
|
- the English and Russian shell catalogs;
|
||||||
|
- loaded plugin catalogs keyed by plugin ID and locale;
|
||||||
|
- a subscription API for Svelte shell components and plugin bridges;
|
||||||
|
- named parameter interpolation.
|
||||||
|
|
||||||
|
The shell-facing API is synchronous after initialization:
|
||||||
|
|
||||||
|
```js
|
||||||
|
getLanguagePreference()
|
||||||
|
getLocale()
|
||||||
|
t(key, params?, fallback?)
|
||||||
|
setLanguagePreference(preference)
|
||||||
|
subscribe(listener)
|
||||||
|
loadPluginCatalog(pluginId, localizationConfig)
|
||||||
|
translatePlugin(pluginId, key, params?, fallback?)
|
||||||
|
```
|
||||||
|
|
||||||
|
Application startup loads the persisted preference before the main interactive
|
||||||
|
shell is shown. Catalog-loading failures do not prevent startup.
|
||||||
|
|
||||||
|
Parameter interpolation supports named placeholders such as `{count}`. Missing
|
||||||
|
parameters leave the placeholder visible so catalog mistakes are detectable.
|
||||||
|
Catalog strings are always assigned through existing safe text rendering APIs;
|
||||||
|
translation values are not treated as HTML.
|
||||||
|
|
||||||
|
## Plugin Manifest Contract
|
||||||
|
|
||||||
|
Add an optional top-level manifest field:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"localization": {
|
||||||
|
"defaultLocale": "en",
|
||||||
|
"locales": {
|
||||||
|
"en": "locales/en.json",
|
||||||
|
"ru": "locales/ru.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The field belongs at the top level because manifest metadata and contributions
|
||||||
|
can be localized even when a plugin has no frontend bundle.
|
||||||
|
|
||||||
|
Validation rules:
|
||||||
|
|
||||||
|
- `defaultLocale` and locale map keys use lower-case supported locale tags;
|
||||||
|
- the default locale must have a declared catalog;
|
||||||
|
- catalog paths must be plugin-relative safe paths;
|
||||||
|
- absolute paths, traversal, backslashes, and paths outside the plugin root are
|
||||||
|
rejected;
|
||||||
|
- localization is optional, preserving compatibility with existing and
|
||||||
|
third-party plugins.
|
||||||
|
|
||||||
|
Catalog files are flat JSON string maps. Official plugins provide both `en`
|
||||||
|
and `ru` catalogs. The English manifest literals stay in place as readable
|
||||||
|
fallbacks and for compatibility with hosts that do not implement localization.
|
||||||
|
|
||||||
|
## Manifest And Contribution Keys
|
||||||
|
|
||||||
|
Desktop derives stable keys rather than embedding translation tokens in fields.
|
||||||
|
|
||||||
|
Reserved keys:
|
||||||
|
|
||||||
|
```text
|
||||||
|
manifest.name
|
||||||
|
manifest.description
|
||||||
|
contributions.views.<id>.title
|
||||||
|
contributions.commands.<id>.title
|
||||||
|
contributions.settingsPanels.<id>.title
|
||||||
|
contributions.sidebarItems.<id>.title
|
||||||
|
contributions.fileActions.<id>.title
|
||||||
|
contributions.noteActions.<id>.title
|
||||||
|
contributions.contextMenuEntries.<id>.title
|
||||||
|
contributions.searchProviders.<id>.label
|
||||||
|
contributions.statusBarItems.<id>.label
|
||||||
|
contributions.workspaceItems.<id>.title
|
||||||
|
```
|
||||||
|
|
||||||
|
The complete set follows contribution fields defined by the SDK schema. IDs
|
||||||
|
are used verbatim after the contribution type prefix. Missing keys retain the
|
||||||
|
literal manifest value.
|
||||||
|
|
||||||
|
The desktop localizes copies returned to frontend presentation code; it does
|
||||||
|
not mutate the backend registry or change IDs, handlers, capabilities, or
|
||||||
|
permission logic.
|
||||||
|
|
||||||
|
## Plugin Runtime API
|
||||||
|
|
||||||
|
Extend `VerstakPluginAPI` with:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
i18n: {
|
||||||
|
getLocale(): 'ru' | 'en';
|
||||||
|
t(key: string, params?: Record<string, string | number>, fallback?: string): string;
|
||||||
|
onDidChangeLocale(listener: (locale: 'ru' | 'en') => void): Unsubscribe;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The desktop preloads a plugin catalog before mounting its component. `t` is
|
||||||
|
therefore synchronous during rendering. The API automatically disposes locale
|
||||||
|
subscriptions with the rest of the plugin API.
|
||||||
|
|
||||||
|
Official plugin components subscribe once when mounted and rerender only their
|
||||||
|
text when the locale changes. Locale changes must not clear form values,
|
||||||
|
selection, dirty editor state, loaded data, or navigation state. The host does
|
||||||
|
not solve localization by unmounting and remounting plugin components.
|
||||||
|
|
||||||
|
Plugins without localization metadata continue to work. Their `t` calls return
|
||||||
|
the supplied fallback or key, and their literal manifest metadata remains
|
||||||
|
visible.
|
||||||
|
|
||||||
|
## Catalog Loading Boundary
|
||||||
|
|
||||||
|
Desktop exposes a dedicated read-only backend method for a declared plugin
|
||||||
|
catalog. It locates the enabled/discovered plugin, selects only a path declared
|
||||||
|
by its localization manifest, validates containment within the plugin root, and
|
||||||
|
returns a parsed string map or an error.
|
||||||
|
|
||||||
|
Do not repurpose arbitrary plugin settings or make localization catalogs
|
||||||
|
writable at runtime. Do not give plugins access to another plugin's catalog.
|
||||||
|
|
||||||
|
Catalog errors are isolated:
|
||||||
|
|
||||||
|
- malformed selected-locale catalog: fall back to default locale;
|
||||||
|
- missing default catalog: use manifest literals and key/fallback values;
|
||||||
|
- one broken plugin catalog: report a diagnostic without affecting other
|
||||||
|
plugins or the shell.
|
||||||
|
|
||||||
|
## Fallback Rules
|
||||||
|
|
||||||
|
Shell translation fallback:
|
||||||
|
|
||||||
|
```text
|
||||||
|
resolved locale -> English catalog -> explicit fallback -> key
|
||||||
|
```
|
||||||
|
|
||||||
|
Plugin translation fallback:
|
||||||
|
|
||||||
|
```text
|
||||||
|
resolved locale -> plugin default locale -> explicit fallback -> key
|
||||||
|
```
|
||||||
|
|
||||||
|
Manifest/contribution fallback:
|
||||||
|
|
||||||
|
```text
|
||||||
|
resolved locale -> plugin default locale -> original manifest literal
|
||||||
|
```
|
||||||
|
|
||||||
|
Russian and English catalogs must have the same keys in CI for the shell and
|
||||||
|
official plugins. Third-party plugins may provide only their default locale.
|
||||||
|
|
||||||
|
## Scope Of Translated UI
|
||||||
|
|
||||||
|
The first milestone translates:
|
||||||
|
|
||||||
|
- vault onboarding and shell navigation;
|
||||||
|
- status bar, settings menu, command palette, global search chrome, dialogs,
|
||||||
|
plugin host states, and Plugin Manager;
|
||||||
|
- all normal, empty, loading, confirmation, warning, and known validation
|
||||||
|
states in official plugins;
|
||||||
|
- official plugin names, descriptions, contribution titles, and labels;
|
||||||
|
- accessibility labels and tooltips owned by shell or official plugins.
|
||||||
|
|
||||||
|
Arbitrary backend, operating-system, filesystem, network, and third-party error
|
||||||
|
messages remain in their source language. The surrounding user-facing prefix
|
||||||
|
and recovery instruction are translated. Stable known error codes may receive
|
||||||
|
localized messages in later milestones.
|
||||||
|
|
||||||
|
User data, filenames, workspace names, provider values, logs, identifiers, API
|
||||||
|
names, and developer diagnostics are never translated.
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
Use TDD for each production change.
|
||||||
|
|
||||||
|
### SDK
|
||||||
|
|
||||||
|
- manifest schema accepts valid localization declarations;
|
||||||
|
- schema rejects unsafe or incomplete declarations;
|
||||||
|
- TypeScript types expose localization metadata and `api.i18n`;
|
||||||
|
- mock API implements locale reads, translations, and subscriptions;
|
||||||
|
- interpolation and fallback behavior are covered.
|
||||||
|
|
||||||
|
### Desktop backend
|
||||||
|
|
||||||
|
- missing language defaults to `system`;
|
||||||
|
- all three accepted values persist and reload;
|
||||||
|
- invalid values are rejected;
|
||||||
|
- language updates preserve unrelated settings;
|
||||||
|
- catalog reads accept only declared safe paths;
|
||||||
|
- malformed, missing, and traversal catalog cases fail safely.
|
||||||
|
|
||||||
|
### Desktop frontend
|
||||||
|
|
||||||
|
- system locale resolution maps `ru-*` to `ru` and others to `en`;
|
||||||
|
- shell fallback and interpolation are deterministic;
|
||||||
|
- language menu shows and persists the selected option;
|
||||||
|
- changing language updates visible shell text without reload;
|
||||||
|
- plugin API receives the locale and a working catalog;
|
||||||
|
- contribution labels change without changing contribution identity;
|
||||||
|
- missing plugin catalogs preserve English manifest literals.
|
||||||
|
|
||||||
|
### Official plugins
|
||||||
|
|
||||||
|
- every official manifest declares English and Russian catalogs;
|
||||||
|
- catalog key parity is checked;
|
||||||
|
- every manifest/contribution localization key is present;
|
||||||
|
- focused smoke tests exercise English and Russian rendering;
|
||||||
|
- a mounted component reacts to a locale change without losing its state.
|
||||||
|
|
||||||
|
### End-to-end
|
||||||
|
|
||||||
|
- first launch with a mocked Russian system locale renders Russian;
|
||||||
|
- first launch with another locale renders English;
|
||||||
|
- English -> Russian -> System selection persists through reload;
|
||||||
|
- shell and representative plain-JS and Svelte plugins switch together;
|
||||||
|
- plugin enable/disable and failed-plugin isolation still work.
|
||||||
|
|
||||||
|
## Rollout Order
|
||||||
|
|
||||||
|
1. SDK manifest/types/mock contract.
|
||||||
|
2. Desktop setting and catalog security boundary.
|
||||||
|
3. Desktop localization service and language menu.
|
||||||
|
4. Desktop shell catalog migration.
|
||||||
|
5. Official plugin manifests, catalogs, and runtime UI migration.
|
||||||
|
6. Full unit, smoke, build, and mocked-Wails E2E verification.
|
||||||
|
7. Real Wails/WebKit GUI smoke when the native environment is available.
|
||||||
|
|
||||||
|
The work remains split into small commits per repository. Existing unrelated
|
||||||
|
changes, including generated Wails bindings already present in the desktop
|
||||||
|
worktree, must not be included.
|
||||||
|
|
||||||
|
## Follow-up Work
|
||||||
|
|
||||||
|
After this localization milestone:
|
||||||
|
|
||||||
|
1. add browser-extension `_locales` catalogs and its independent language
|
||||||
|
behavior;
|
||||||
|
2. introduce stable workspace IDs so renames do not orphan plugin scopes;
|
||||||
|
3. scope sync blobs by tenant/vault and make operation-sequence writes
|
||||||
|
transactional;
|
||||||
|
4. make concurrent settings updates lossless;
|
||||||
|
5. remove remaining hard-coded official plugin IDs and schemas from shell;
|
||||||
|
6. reduce drift between E2E mock bundles and real plugin packages;
|
||||||
|
7. include whole-workspace entries in global Trash where appropriate.
|
||||||
|
|
@ -0,0 +1,510 @@
|
||||||
|
# First Alpha Product UX Design
|
||||||
|
|
||||||
|
## Status and scope
|
||||||
|
|
||||||
|
This is the approved product-UX tranche for the first public alpha. It covers
|
||||||
|
the Desktop application, official Activity, Journal and Browser Inbox plugins,
|
||||||
|
and the browser extension. Release packaging, public repository documentation,
|
||||||
|
licensing, and sync-server hardening are specified separately in the
|
||||||
|
`alpha-release` tranche.
|
||||||
|
|
||||||
|
The user-visible Russian term is **Дело**. Existing platform and storage names
|
||||||
|
such as `workspaceRootPath` remain internal compatibility details in this
|
||||||
|
tranche.
|
||||||
|
|
||||||
|
## Durable identity of a Дело
|
||||||
|
|
||||||
|
Every managed Дело has an immutable UUID v4 `workspaceId`. It is the identity for
|
||||||
|
relations; `workspaceRootPath` is only the current or historical filesystem
|
||||||
|
address and presentation label. Inbox assignments, domain bindings, Activity
|
||||||
|
events/sessions/candidates, Journal source references, and Overview state store
|
||||||
|
`workspaceId` as their primary relation key and retain a path only as a cached
|
||||||
|
display value.
|
||||||
|
|
||||||
|
The UUID lives in a small immutable marker inside the case folder,
|
||||||
|
`<Дело>/.verstak/workspace.json`, and is also indexed in Desktop metadata. The
|
||||||
|
inside-folder marker survives a Desktop rename, Trash move, restore, and an
|
||||||
|
external filesystem rename. It prevents a newly created folder with the same
|
||||||
|
path from inheriting old links.
|
||||||
|
|
||||||
|
On first alpha startup, each writable legacy or externally created top-level
|
||||||
|
case without a marker receives a new UUID and its path-keyed relation data is
|
||||||
|
migrated to that UUID. If a folder is not writable, it remains viewable but
|
||||||
|
cannot be selected as a durable relation target until its marker can be
|
||||||
|
created. If two active folders contain the same UUID (for example after a
|
||||||
|
filesystem copy), Desktop shows an identity-repair action and does not
|
||||||
|
automatically attach Inbox, binding, or Activity data to either duplicate.
|
||||||
|
The repair UI asks which folder retains the existing identity; it generates a
|
||||||
|
new marker UUID for the other folder and leaves old relations with the retained
|
||||||
|
identity. It never silently merges the two folders' histories.
|
||||||
|
|
||||||
|
Workspace lifecycle events carry both `workspaceId` and current path. A rename
|
||||||
|
updates only the path cache. Trash and restore retain the same ID and use the
|
||||||
|
trash ID only to match the particular trash operation. When a folder is removed
|
||||||
|
outside Desktop, its relations retain the UUID and become unavailable; a new
|
||||||
|
folder at the old path has a newly generated UUID and cannot take them over.
|
||||||
|
|
||||||
|
This document supersedes conflicting decisions in the following older narrow
|
||||||
|
designs:
|
||||||
|
|
||||||
|
- `2026-06-29-activity-worklog-suggestions-design.md` for candidate lifecycle
|
||||||
|
and background availability;
|
||||||
|
- `2026-06-29-browser-inbox-domain-binding-design.md` for the relation between
|
||||||
|
bindings, global Inbox, and browser activity;
|
||||||
|
- `2026-07-11-platform-localization-design.md` only where it would leave
|
||||||
|
developer-facing plugin IDs visible in the normal user interface.
|
||||||
|
|
||||||
|
## Product decisions
|
||||||
|
|
||||||
|
- A new Дело is created **only** by an explicit user action. Browser Inbox,
|
||||||
|
Activity, and Journal never create a case, candidate case, or implicit case.
|
||||||
|
- A manual extension send is a Browser Inbox capture. Passive browsing activity
|
||||||
|
is never an Inbox capture.
|
||||||
|
- Passive browser activity records only a normalized hostname and measured
|
||||||
|
duration. It never records a page URL, title, selection, page content,
|
||||||
|
keystrokes, or browsing history.
|
||||||
|
- Passive browser tracking is disabled on a new installation. It starts only
|
||||||
|
after explicit, informed consent in extension settings.
|
||||||
|
- Passive browser activity counts only the active tab in a focused browser
|
||||||
|
window. Background tabs and unfocused browser windows contribute no time.
|
||||||
|
- Activity can suggest a Journal record, but the user reviews and saves it.
|
||||||
|
Journal entries are never created automatically.
|
||||||
|
- An unassigned activity can be saved only after the user picks an existing
|
||||||
|
Дело. Picking it for a single Journal entry does not silently create a domain
|
||||||
|
binding.
|
||||||
|
- The first alpha retains one optional Дело assignment per Browser Inbox
|
||||||
|
capture, matching current product semantics. It does not introduce sharing a
|
||||||
|
capture between several cases.
|
||||||
|
|
||||||
|
## Browser extension: manual capture and passive activity
|
||||||
|
|
||||||
|
### Manual capture
|
||||||
|
|
||||||
|
The existing explicit popup/context-menu actions remain the only way to create
|
||||||
|
`browser.capture.*` events and Browser Inbox records. Their protocol and
|
||||||
|
retry queue remain separate from passive activity data.
|
||||||
|
|
||||||
|
### Domain activity tracker
|
||||||
|
|
||||||
|
Passive tracking is an opt-in extension setting, `passiveActivityEnabled`, with
|
||||||
|
a default of `false`. The first extension settings view includes a concise
|
||||||
|
consent card and an unchecked switch. It states that the feature sends only a
|
||||||
|
normalized hostname and duration, and explicitly states that it does **not**
|
||||||
|
collect or send URLs, titles, page content, selections, keystrokes, or browser
|
||||||
|
history. The same explanation remains next to the switch after onboarding.
|
||||||
|
|
||||||
|
Until the user enables that switch, the extension does not subscribe to
|
||||||
|
tracking events, create activity state, or send activity records. Disabling it
|
||||||
|
stops tracking immediately, clears only the mutable unflushed accumulator, and
|
||||||
|
leaves already acknowledged Desktop activity unchanged. The user can either
|
||||||
|
retry or discard already-created pending batches through an explicit settings
|
||||||
|
action; disabling tracking never silently loses them.
|
||||||
|
|
||||||
|
When enabled, the extension uses a persisted tracker:
|
||||||
|
|
||||||
|
1. When an HTTP(S) page becomes the active tab of a focused browser window,
|
||||||
|
start timing its normalized lowercase hostname.
|
||||||
|
2. On tab activation, hostname change, window focus loss, browser idle/lock,
|
||||||
|
or a five-minute alarm, write a checkpoint and calculate elapsed time. The
|
||||||
|
idle detection threshold is explicitly ten minutes; a locked state pauses
|
||||||
|
immediately.
|
||||||
|
3. Ignore browser-internal pages, invalid URLs, and excluded hostnames. An
|
||||||
|
exclusion `youtube.com` matches that hostname and every subdomain; the same
|
||||||
|
rule applies to `x.com`.
|
||||||
|
4. On a flush, freeze one record per hostname as a `browser.activity.domain`
|
||||||
|
batch. The payload contains a schema version, idempotency ID, observed
|
||||||
|
period bounds, hostname, and `durationSeconds`; it contains no URL-like
|
||||||
|
field.
|
||||||
|
|
||||||
|
The persisted state has three distinct parts:
|
||||||
|
|
||||||
|
```text
|
||||||
|
activeAccumulator: mutable, unsent duration and bounds by hostname
|
||||||
|
pendingBatches: immutable payloads, keyed by idempotency ID
|
||||||
|
acknowledgedIds: bounded recent acknowledgement IDs
|
||||||
|
```
|
||||||
|
|
||||||
|
Flushing copies a hostname's current accumulator into a new immutable pending
|
||||||
|
batch with a newly generated ID, then clears only that copied accumulator. New
|
||||||
|
time for the same hostname accumulates in `activeAccumulator` and can become a
|
||||||
|
later batch. It never mutates an already sent batch. Retries send the stored
|
||||||
|
payload byte-for-byte. A successful acknowledgement removes only the matching
|
||||||
|
pending batch and records its ID; it cannot remove newer time. Pending batches
|
||||||
|
are sent oldest first. `acknowledgedIds` is a 30-day bounded LRU set used to
|
||||||
|
handle replayed acknowledgement messages safely; Desktop maintains its own
|
||||||
|
idempotency store as the authority.
|
||||||
|
|
||||||
|
The tracker uses timestamp arithmetic but applies a conservative ambiguity
|
||||||
|
limit. A checkpoint contributes elapsed time only if wall-clock time is
|
||||||
|
monotonic and the gap from the previous trustworthy checkpoint is no more than
|
||||||
|
ten minutes. A negative clock delta, a gap over ten minutes, an idle/locked
|
||||||
|
state, or a browser startup after a crash discards the ambiguous interval and
|
||||||
|
establishes a fresh baseline. WebExtensions have no portable system
|
||||||
|
suspend/resume event, so the first post-suspend observation is deliberately
|
||||||
|
handled by this gap rule; platform idle/lock events add an earlier pause where
|
||||||
|
available. This can undercount ambiguous work but cannot turn an overnight
|
||||||
|
sleep or a clock change into working time. A browser startup never carries an
|
||||||
|
active interval across process death; pending batches and accumulated completed
|
||||||
|
intervals are retained.
|
||||||
|
|
||||||
|
The extension persists timestamps and the three state parts in local extension
|
||||||
|
storage, so a Manifest V3 service-worker restart can continue safely. It sets
|
||||||
|
the `idle` API detection interval to ten minutes. Its manifest gains the
|
||||||
|
`windows`, `alarms`, and `idle` permissions needed for this flow; existing tab
|
||||||
|
access is retained. Firefox uses the equivalent WebExtension events when
|
||||||
|
available and otherwise applies the same ten-minute checkpoint limit.
|
||||||
|
|
||||||
|
The settings page gets an **Excluded domains** list. It accepts one hostname per
|
||||||
|
item through the canonical normalization below, and explains that a hostname
|
||||||
|
excludes its subdomains. The default list is empty. Adding an exclusion stops
|
||||||
|
the matching active measurement immediately and discards only its mutable
|
||||||
|
unflushed time; immutable pending batches remain available for the user's
|
||||||
|
explicit retry or discard decision.
|
||||||
|
|
||||||
|
### Canonical hostname normalization
|
||||||
|
|
||||||
|
Every extension event, exclusion, and Desktop domain binding uses
|
||||||
|
`hostname-normalization-v1`. It is a normative shared contract, not an
|
||||||
|
implementation detail of one component:
|
||||||
|
|
||||||
|
1. An activity source must be an HTTP(S) URL. Its port, user info, path, query,
|
||||||
|
and fragment are discarded before hostname normalization. A binding or
|
||||||
|
exclusion must be a bare hostname; schemes, paths, queries, fragments, and
|
||||||
|
ports are rejected.
|
||||||
|
2. Trim surrounding whitespace, lowercase, and remove exactly one DNS root
|
||||||
|
trailing dot. `example.com.` therefore becomes `example.com`.
|
||||||
|
3. Convert DNS names using non-transitional UTS #46 / IDNA lookup processing to
|
||||||
|
an ASCII A-label. The canonical stored and compared form of `пример.рф` is
|
||||||
|
its punycode A-label; the settings UI may render the Unicode display form.
|
||||||
|
4. IPv4 addresses are accepted in canonical dotted-decimal form. IPv6 literals
|
||||||
|
are accepted (bracketed only at URL/settings input), stored without brackets
|
||||||
|
in canonical lower-case RFC 5952 form, and never include a port. `localhost`
|
||||||
|
and syntactically valid single-label internal names are also accepted.
|
||||||
|
5. Reject empty values, malformed IP literals, invalid labels, empty labels,
|
||||||
|
labels over 63 ASCII bytes, DNS names over 253 ASCII bytes, and any input
|
||||||
|
that fails URL/IDNA parsing. Invalid activity is not counted; invalid
|
||||||
|
settings input gets an inline validation error and is not saved.
|
||||||
|
|
||||||
|
The SDK owns a versioned `hostname-normalization-v1` test-vector corpus. Desktop
|
||||||
|
and the browser extension vendor the byte-identical corpus in their tests; the
|
||||||
|
coordinated `build-all` verification checks its hash. Go and JavaScript have
|
||||||
|
separate implementations, but both must pass every vector, including trailing
|
||||||
|
dots, Unicode/punycode equivalence, IPv4, IPv6, localhost, internal names,
|
||||||
|
ports, malformed input, and excessive lengths.
|
||||||
|
|
||||||
|
### Desktop receiver
|
||||||
|
|
||||||
|
Desktop exposes an authenticated activity receiver separate from the capture
|
||||||
|
receiver. It normalizes and validates a hostname with
|
||||||
|
`hostname-normalization-v1`, validates a positive bounded duration, ISO time
|
||||||
|
fields, schema version, and idempotency ID before it publishes
|
||||||
|
`browser.activity.domain`. Invalid and duplicate records do not enter Activity.
|
||||||
|
The existing local pairing token gates the endpoint; the token is never placed
|
||||||
|
in Activity storage or UI.
|
||||||
|
|
||||||
|
The receiver annotates a valid activity with an existing exact hostname-to-Дело
|
||||||
|
binding when one exists. A binding stores `workspaceId` as identity and the
|
||||||
|
current root path as display cache. Bindings remain explicit:
|
||||||
|
`client.example.com` does not imply `example.com` or the reverse. This is
|
||||||
|
deliberately different from the exclusion-list suffix rule. Unbound activity is
|
||||||
|
stored with the explicit `unassigned` session scope.
|
||||||
|
|
||||||
|
## Activity and Journal
|
||||||
|
|
||||||
|
### Background processing
|
||||||
|
|
||||||
|
Official plugins gain a lifecycle-safe background-service contribution. It is
|
||||||
|
loaded when the plugin host starts, not only while the plugin view is mounted.
|
||||||
|
The Activity service subscribes once to public events, normalizes and persists
|
||||||
|
them, rebuilds candidates, and releases subscriptions on host teardown.
|
||||||
|
|
||||||
|
The platform provides unsubscribe-capable event subscriptions. Command
|
||||||
|
registration is owned by the background service so commands remain available
|
||||||
|
without opening the Activity view.
|
||||||
|
|
||||||
|
Raw Activity data is not held in `settings.json`. The Desktop storage layer
|
||||||
|
provides a lifecycle-safe, plugin-scoped append-only event log for
|
||||||
|
`verstak.activity` under plugin data. Appending one event does not rewrite the
|
||||||
|
whole log. Plugin settings retain only compact preferences; plugin data retains
|
||||||
|
candidate watermarks and indexes. Appends and compaction are serialized by the
|
||||||
|
storage layer, preventing lost updates.
|
||||||
|
|
||||||
|
### Candidate rules
|
||||||
|
|
||||||
|
Activity presents chronological sessions rather than a raw log by default. A
|
||||||
|
browser activity session displays, for example, `admin.client-site.ru · 1 ч
|
||||||
|
32 мин`; it exposes no hidden URL/title data.
|
||||||
|
|
||||||
|
A logical session has one explicit scope:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{ kind: "workspace", workspaceId, workspaceRootPath } | { kind: "unassigned" }
|
||||||
|
```
|
||||||
|
|
||||||
|
An unassigned session is a normal temporal scope, not an exception. It is shown
|
||||||
|
in Activity and may open Journal review, where the user must choose an existing
|
||||||
|
active Дело. That one-time choice does not create a binding. A workspace session
|
||||||
|
is split only by a different workspace ID, a transition to/from unassigned, a
|
||||||
|
20-minute idle gap, or 120 minutes of total session span. It is not split
|
||||||
|
merely because midnight passes. A session is ready when either:
|
||||||
|
|
||||||
|
- meaningful events in its session cover at least ten minutes and include at
|
||||||
|
least two events; or
|
||||||
|
- it contains one or more browser-domain records totaling at least ten minutes.
|
||||||
|
|
||||||
|
`workspace.selected`, `file.opened`, and `note.opened` are diagnostic context,
|
||||||
|
not meaningful work by themselves.
|
||||||
|
|
||||||
|
Duration is normative rather than the wall-clock span from first to last event.
|
||||||
|
Sort meaningful events by `{occurredAt, activityId}` within a session:
|
||||||
|
|
||||||
|
- A browser-domain record contributes its explicit validated
|
||||||
|
`durationSeconds`.
|
||||||
|
- A file/note event has no inherent duration. For each adjacent pair of
|
||||||
|
zero-duration meaningful events, add `min(time difference, 10 minutes)` only
|
||||||
|
when no explicit-duration browser record lies between that pair in the
|
||||||
|
ordered session.
|
||||||
|
- A gap over 20 minutes has already split the session, so it contributes no
|
||||||
|
implicit duration. The first and final standalone point event each contribute
|
||||||
|
zero seconds.
|
||||||
|
- Candidate duration is the sum of explicit browser duration and these implicit
|
||||||
|
point-event intervals, capped at the 120-minute session maximum. It is never
|
||||||
|
inferred from the overall first-to-last span.
|
||||||
|
|
||||||
|
Thus a note saved at 10:00 and a file changed at 10:09 estimate nine minutes;
|
||||||
|
the same events at 10:00 and 10:50 form separate zero-duration sessions rather
|
||||||
|
than a fictional 50-minute block.
|
||||||
|
|
||||||
|
Each logical session receives a newly generated immutable UUID `sessionId` at
|
||||||
|
creation and stores an immutable anchor `{scope, firstSeenAt, firstActivityId}`.
|
||||||
|
The service stores that ID on each appended event and preserves the anchor
|
||||||
|
through log compaction. A late event may join a same-scope session only when it
|
||||||
|
falls within that session's 20-minute boundary; otherwise it starts a new
|
||||||
|
session. It never recomputes an existing session ID or anchor. Each session
|
||||||
|
persists an ordered handled watermark
|
||||||
|
`{ occurredAt, activityId }` and optional state for the latest reviewed slice.
|
||||||
|
A candidate contains only source events after that watermark:
|
||||||
|
|
||||||
|
- **Accepting** a candidate stores its `sessionId` and handled watermark on the
|
||||||
|
Journal entry. Later activity can create a candidate only for the additional
|
||||||
|
interval after that watermark and only when that new interval reaches the
|
||||||
|
normal threshold.
|
||||||
|
- **Dismissing** consumes the current candidate slice through its watermark.
|
||||||
|
`A+B` therefore cannot reappear as `A+B+C`; only qualifying new work after
|
||||||
|
`B` may later be suggested.
|
||||||
|
- A dismissal is not a permanent ban on future work in the same logical
|
||||||
|
session. At least ten new meaningful minutes are required before it can be
|
||||||
|
suggested again.
|
||||||
|
|
||||||
|
An event arriving late at or before a handled watermark remains in the raw
|
||||||
|
diagnostic log but never reopens an accepted or dismissed candidate slice.
|
||||||
|
|
||||||
|
An across-midnight logical session remains one candidate. The review displays
|
||||||
|
its time apportioned by local date, preselects the date containing the largest
|
||||||
|
share of the duration (ties use the start date), and lets the user choose the
|
||||||
|
Journal date. This preserves a 23:50–00:30 session instead of losing its first
|
||||||
|
ten minutes to an artificial threshold.
|
||||||
|
|
||||||
|
The Journal review action opens the existing journal editor with candidate
|
||||||
|
duration, date, and a concise domain/event summary. The user may edit all
|
||||||
|
content and must explicitly save. For global/unassigned activity, the review
|
||||||
|
requires choosing one existing Дело first. Missing or disabled Journal support
|
||||||
|
produces a visible, actionable message rather than failing silently.
|
||||||
|
|
||||||
|
Activity has two clear views:
|
||||||
|
|
||||||
|
- **Сессии** — default, with candidate cards and activity summaries;
|
||||||
|
- **События** — a secondary diagnostic stream for technical event inspection.
|
||||||
|
|
||||||
|
### Retention and deletion
|
||||||
|
|
||||||
|
The alpha retains raw Activity events for at most 60 days, 10,000 events, or
|
||||||
|
8 MiB of log data, whichever limit is reached first. On append and at service
|
||||||
|
startup, bounded compaction removes the oldest entries until all limits hold;
|
||||||
|
it does not block the UI. Candidate state is pruned once its session and every
|
||||||
|
related Journal source watermark are older than 60 days.
|
||||||
|
|
||||||
|
Saving a Journal entry does not delete its source Activity events. The Journal
|
||||||
|
entry retains the compact source session/watermark reference after raw-event
|
||||||
|
retention removes those events. **Clear activity for this Дело** removes only
|
||||||
|
that case's raw events, sessions, and candidate state after confirmation.
|
||||||
|
**Clear all activity** is a separate global confirmed operation.
|
||||||
|
|
||||||
|
## Browser Inbox lifecycle
|
||||||
|
|
||||||
|
### Record state
|
||||||
|
|
||||||
|
A capture remains one canonical record with independent fields:
|
||||||
|
|
||||||
|
```text
|
||||||
|
globalState: active | archived
|
||||||
|
workspaceId: optional immutable UUID of assigned Дело
|
||||||
|
workspaceState: unassigned | active | trashed | unavailable | orphaned
|
||||||
|
workspaceRootPath: optional current or historical Дело path cache
|
||||||
|
workspaceTrashId: optional stable trash identity
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing records migrate to `globalState: active`. An assignment whose current
|
||||||
|
path resolves to an active marker receives that marker's `workspaceId`; an
|
||||||
|
unresolved legacy path becomes `unavailable` rather than attaching to a future
|
||||||
|
folder of the same name. The current test vault may be discarded, but the
|
||||||
|
migration is non-destructive for an alpha user vault.
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
- **Assign to Дело** retains the capture in global Inbox and shows it in the
|
||||||
|
selected Дело's Inbox.
|
||||||
|
- **Убрать из общих входящих** changes `globalState` to `archived`. It never
|
||||||
|
removes the assignment, so an assigned capture remains available from its
|
||||||
|
Дело.
|
||||||
|
- **Открепить от дела** clears `workspaceId`, `workspaceRootPath`, and
|
||||||
|
`workspaceTrashId`, and sets `workspaceState` to `unassigned`. If the capture is
|
||||||
|
still globally active it returns to the global Inbox; otherwise it remains in
|
||||||
|
archive.
|
||||||
|
- **Удалить везде** removes the canonical capture and its assignment. It is a
|
||||||
|
separate destructive action and requires confirmation that names the affected
|
||||||
|
Дело when assigned.
|
||||||
|
- **Сохранить ссылку в деле** is available only for a valid HTTP(S) capture URL.
|
||||||
|
It opens a small save dialog with an editable proposed filename derived from
|
||||||
|
the capture title, then its hostname, then `Link`. The stem is sanitized for
|
||||||
|
cross-platform forbidden characters and control characters, limited to 96
|
||||||
|
characters, and receives `.url`. The file uses InternetShortcut format and
|
||||||
|
is created through the plugin `files.write` capability under the case's
|
||||||
|
`Links/` directory, creating that directory first. A name collision never
|
||||||
|
overwrites silently: the dialog offers an explicit `name (2).url` alternative
|
||||||
|
or cancellation. Read-only/error cases leave the capture unchanged and show
|
||||||
|
the failure. Success publishes the normal safe file/link Activity event; the
|
||||||
|
link is opened through the dedicated URL-opening behaviour below, not by
|
||||||
|
relying on a Linux `.url` file association.
|
||||||
|
|
||||||
|
Bulk actions operate only on the visibly filtered capture set, show the count,
|
||||||
|
and use archive rather than permanent deletion. Every permanent deletion and
|
||||||
|
Journal deletion has a cancellation-safe confirmation dialog.
|
||||||
|
|
||||||
|
When a Дело is renamed, the Inbox service updates the path cache of captures
|
||||||
|
and exact domain bindings with that `workspaceId`; no relation is keyed by the
|
||||||
|
old root path. It does not update a binding that has been manually changed to a
|
||||||
|
different workspace ID during that operation.
|
||||||
|
|
||||||
|
When a Дело goes to Trash, assignments and bindings for its `workspaceId`
|
||||||
|
become `trashed` with the Desktop trash ID. They remain visible as unavailable
|
||||||
|
historical context but are not used for automatic routing. A restore event
|
||||||
|
carrying that trash ID restores the same ID's assignment/binding, including a
|
||||||
|
restored path changed by a collision. A permanent trash purge changes captures
|
||||||
|
to unassigned and changes bindings to a visible `orphaned` state that the user
|
||||||
|
can reassign or remove. Activity remains historical and labels the case as
|
||||||
|
deleted. If a case disappears by external filesystem change, active references
|
||||||
|
become `unavailable`, automatic routing is disabled, and the user must
|
||||||
|
explicitly reassign or remove them; a newly created folder with the same name
|
||||||
|
has a different `workspaceId` and never steals those references.
|
||||||
|
|
||||||
|
### Archive and filters
|
||||||
|
|
||||||
|
Global Browser Inbox has **Active**, **Archive**, and **All** status filters;
|
||||||
|
Active is the default. Search applies within the chosen filter, so Archive is
|
||||||
|
searchable deliberately through Archive or All rather than unexpectedly
|
||||||
|
appearing in the normal queue. **Restore to Inbox** changes `globalState` back
|
||||||
|
to `active`. Archive supports a visible filtered bulk restore with a count and
|
||||||
|
confirmation. A capture assigned to a Дело remains visible in that Дело's Inbox
|
||||||
|
regardless of its global archive state, with an Archive badge.
|
||||||
|
|
||||||
|
### Opening saved links on Linux
|
||||||
|
|
||||||
|
The platform adds a user-initiated `urls.openExternal` capability and API. It
|
||||||
|
accepts only a validated HTTP(S) URL and opens that URL through the system
|
||||||
|
browser opener (`xdg-open` in the Linux alpha); it never passes a `.url` file
|
||||||
|
path to the opener. Browser Inbox uses this capability for **Open link** and
|
||||||
|
for a saved `.url` after parsing and validating its `URL=` value. The Files
|
||||||
|
surface recognizes a valid `.url` file and uses the same URL-opening path. This
|
||||||
|
does not depend on desktop file-association support for InternetShortcut files.
|
||||||
|
|
||||||
|
## Alpha interface
|
||||||
|
|
||||||
|
- Use Russian product labels consistently: **Дела**, **Входящие**,
|
||||||
|
**Активности**, and **Журнал**. User-facing dates use the local time zone.
|
||||||
|
- With a selected Дело, the overview reads only records explicitly scoped to
|
||||||
|
that Дело; an unscoped global event never leaks into every case. With no
|
||||||
|
selected Дело, it shows a short prompt to select/create one plus up to five
|
||||||
|
active unassigned Inbox records; it does not fabricate a blended continuation
|
||||||
|
feed.
|
||||||
|
- **Needs attention** shows at most five entries: ready Journal candidates,
|
||||||
|
active unprocessed Inbox records, and existing urgent Todos, in that order
|
||||||
|
within their respective priority. An Inbox record is new/needs attention
|
||||||
|
while it is active and unprocessed, without an arbitrary age cutoff.
|
||||||
|
- Todo rows are included only when a loaded plugin exposes the `todo.workspace`
|
||||||
|
capability. A missing or disabled Todo plugin is not an Overview error and
|
||||||
|
contributes no placeholder rows.
|
||||||
|
- **Continue work** shows at most four distinct case-scoped entities from the
|
||||||
|
last 14 days: unfinished Todo, unprocessed capture, and the most recent
|
||||||
|
note/file/Journal entity. Every item carries `lastMeaningfulAt`; items sort
|
||||||
|
descending by that value, with an unprocessed capture, Todo, Journal, note,
|
||||||
|
then file as deterministic ties. Multiple `file.changed` events for the same
|
||||||
|
entity collapse to one item. Opening an item does not mark it complete. Its
|
||||||
|
overflow action **Hide recommendation** stores a non-destructive
|
||||||
|
entity/event dismissal; a later meaningful change to that entity makes it
|
||||||
|
eligible again.
|
||||||
|
- **Recent changes** shows at most eight distinct case-scoped records from the
|
||||||
|
last seven days. Included events are note save/create, file create/rename or
|
||||||
|
last change per file, saved browser link, and Journal create. Technical
|
||||||
|
selection/open events are excluded. The empty state says that there were no
|
||||||
|
changes in that period.
|
||||||
|
- Empty states give a next action, not an empty pane.
|
||||||
|
- Clear, delete, and archive actions name their scope and consequences.
|
||||||
|
|
||||||
|
Plugin IDs are diagnostic information. Normal tabs display the manifest's
|
||||||
|
human title, such as `Заметки` or `Изображение`, never
|
||||||
|
`verstak.default-editor.notes-markdown` or `verstak.file-preview.image`.
|
||||||
|
|
||||||
|
Add a persisted **Settings → Debug → Show plugin IDs** preference, defaulting
|
||||||
|
to false. The Desktop application-specific `--debug` command-line argument
|
||||||
|
enables the same display only for that run. When either is active, show the ID
|
||||||
|
adjacent to the human title and in diagnostic errors. Neither mode alters data,
|
||||||
|
plugin permissions, or release behaviour.
|
||||||
|
|
||||||
|
## Error handling and privacy
|
||||||
|
|
||||||
|
- A failed extension delivery keeps the accumulated hostname time locally and
|
||||||
|
reports the pending-batch count and a non-blocking retry state in extension
|
||||||
|
settings; it never falls back to Browser Inbox capture.
|
||||||
|
- Receiver authentication/validation errors provide a safe status to the
|
||||||
|
extension without echoing the pairing token or untrusted payload.
|
||||||
|
- Activity never manufactures a case from missing assignment data.
|
||||||
|
- User-visible errors explain the failed action and offer the next safe action.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Automated checks must cover:
|
||||||
|
|
||||||
|
- browser tracker explicit consent/disable behaviour, focused-tab-only
|
||||||
|
accounting, exclusions, canonical hostname vectors, immutable pending
|
||||||
|
batches, acknowledgement-only reset, retry persistence, payload privacy,
|
||||||
|
negative clock changes, long gaps, restart, lock, and suspend/resume;
|
||||||
|
- Desktop activity receiver authentication, canonical normalization,
|
||||||
|
validation, idempotency, binding, and event publication;
|
||||||
|
- append-only Activity log retention/compaction, background subscription
|
||||||
|
lifecycle, UUID workspace/unassigned session scopes, point-event duration
|
||||||
|
calculation, immutable session IDs/late events, handled watermarks,
|
||||||
|
across-midnight review, Journal handoff, local dates, case-scoped clear, and
|
||||||
|
missing-Journal feedback;
|
||||||
|
- assigning, archiving, restoring, unlinking, permanent deletion, `.url`
|
||||||
|
naming/collision/readonly behaviour, filtered bulk operations, rename, trash
|
||||||
|
restore/purge, external-workspace unavailability, duplicate-workspace-ID
|
||||||
|
repair, and direct Linux URL opening without `.url` association;
|
||||||
|
- normal and debug-mode plugin tab labels;
|
||||||
|
- the corrected frontend Wails mock, deterministic Overview limits/scoping,
|
||||||
|
plus the end-to-end flows activity-to-Journal and Inbox-to-Дело.
|
||||||
|
|
||||||
|
Manual GUI smoke testing verifies Russian normal-mode labels, hidden plugin
|
||||||
|
IDs, candidate review, Inbox preservation, and extension domain exclusions in
|
||||||
|
the Linux Desktop build.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- automatic creation of a Дело;
|
||||||
|
- background-tab or browser-history tracking;
|
||||||
|
- URL/content/title collection for passive activity;
|
||||||
|
- shared/multi-case captures;
|
||||||
|
- automated Journal saving, billing, or time-sheet generation;
|
||||||
|
- browser-extension localization and a general analytics product;
|
||||||
|
- release packaging, licensing, public GitHub README, and sync-server release
|
||||||
|
security, which belong to the next alpha-release design.
|
||||||
Loading…
Reference in New Issue