Files
dchain/docs/ROADMAP.md
vsecoder b55486775e feat(desktop): Electron scaffold, shell, auth + section stubs (v2.2.0-alpha4)
PR #4 of the multi-device roadmap — desktop client groundwork. The shell
compiles and runs end-to-end on top of a v2.2.0 node; sections are
placeholders that later alphas fill in with real chat / feed / wallet /
contacts / settings content shared with the mobile client-app.

Scaffold:
  * Vite + React + TypeScript renderer; Electron main/preload TS
    compiled via a separate tsconfig.
  * npm scripts — `dev` (concurrent Vite + Electron), `build`
    (installer via electron-builder), `typecheck`.
  * electron-builder targets: .dmg / .exe / .AppImage + .deb.
  * CSP pins script-src 'self'; connect-src left open so the renderer
    can hit any configured node.

Electron main + preload:
  * Frame-less window, hiddenInset on macOS, custom-overlay on Windows,
    drag region via CSS -webkit-app-region: drag on our TitleBar.
  * contextIsolation on, nodeIntegration off, sandbox off (needed for
    safeStorage in preload).
  * window.dchain.keyfile.{load,save,delete,encryptionAvailable} —
    keyfile lives in the OS keychain via Electron safeStorage, with a
    plaintext fallback for OSes without an encryption backend.
  * window.dchain.dialog.{openFile,saveFile}, .fs.{readText,writeText},
    .app.{version,platform}. Everything else still goes over plain
    fetch() in the renderer.

Shell (src/shell/):
  * TitleBar — draggable 32px strip; DChain brand.
  * NavBar — left 72px rail, six sections + Cmd+1..5 keybinds.
  * StatusBar — ● online/connecting/offline dot, node URL, current
    chain height (polls /api/netstats every 5s).
  * Shell — composes the 3 panes; picks { List, Detail } by active
    section.

Sections (all stubs — filling in alpha5+):
  * Messages, Feed, Contacts, Profile — SectionPlaceholder with notes.
  * Wallet — shows the balance reading from /api/address/{pub} as a
    first real data binding.
  * Settings — node-URL card with live ping + commit, identity card
    (shows pub key), about card (reads Electron app.version via IPC).

Auth (src/auth/Welcome.tsx):
  * Create — generates Ed25519 + X25519 via tweetnacl, saves via IPC.
  * Import — Electron dialog.openFile → parses node.json → saves.
  * Pair — stub routed; real poll loop reuses the mobile flow in
    alpha5.

Lib (src/lib/):
  * types.ts — KeyFile / Contact / Message / NodeSettings mirroring
    client-app wire formats.
  * storage.ts — keyfile via IPC, settings + contacts + device-registered
    marker via localStorage.
  * api.ts — fetch wrapper with setNodeUrl + onNodeUrlChange;
    getNetStats, getIdentity, fetchDevices, getBalance bindings.
  * store.ts — zustand { booted, keyFile, settings, contacts, section }.

docs/ROADMAP.md — desktop subsection updated with per-alpha breakdown.

Next (alpha5): Messages section wired to the relay mailbox, full
conversation view, and the pairing poll loop.
2026-04-22 17:03:06 +03:00

228 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Roadmap
Living doc — «куда идём дальше». Два активных вектора: **v2.2.0 multi-device**
и **desktop Electron client**. Всё что tentative — помечено `?`; всё что
решено и принято к работе — без знаков.
---
## v2.2.0 — Multi-device (per-device X25519, on-chain device registry)
### Проблема
Сейчас у пользователя одна пара X25519 (хранится в `keyFile`), и relay-mailbox —
queue с DELETE-после-чтения. При двух устройствах:
- Конверт забирает то устройство, что первое дёрнуло `/relay/inbox` — второе
теряет сообщение навсегда.
- История чатов per-device, не синхронизируется (`AsyncStorage` на каждом
устройстве свой).
On-chain часть (posts, follows, tx, wallet) работает нормально — оба устройства
читают один чейн и видят одно и то же. Чинить надо только мессенджер.
### Решение — путь А (Signal-style): X25519 на устройство + device registry
1. **Master identity = Ed25519** (как сейчас). Подписывает tx, владеет
балансом, — один на всю identity.
2. **X25519 — per-device**. Каждое устройство при первом старте генерит
свою пару. Relay-mailbox остаётся queue'ом, но адресуется по
**устройству**, не по identity.
3. **Device registry on-chain**: новые tx-типы `LINK_DEVICE` / `UNLINK_DEVICE`,
подписываются master Ed25519, публикуют/отзывают связку
`(master_pub → device_x25519_pub, device_name)`.
4. **Sender-side fan-out**: при отправке — один envelope на каждое
активное устройство получателя. Мессадж → N конвертов.
5. **Revoke**: master подписывает `UNLINK_DEVICE`, клиент, обнаруживший
свой `x25519_pub` в revoked — wipe'ит локальную БД (zeroize master
Ed25519 + delete chats).
### План работ (PR-by-PR)
#### PR #1 — Chain-side (backend) — _v2.2.0-alpha1_
- [ ] `blockchain/types.go`: `EventLinkDevice`, `EventUnlinkDevice`
+ payload structs `LinkDevicePayload`, `UnlinkDevicePayload`.
- [ ] `blockchain/chain.go`: applyTx-ветки, state persistence:
- `prefixDevice + x25519_pub → DeviceRecord{owner, name, added_at, revoked_at?}`.
- Обратный индекс `prefixDevicesByOwner + master_pub → [x25519_pub, …]`.
- [ ] Константа `MaxDevicesPerOwner = 10`.
- [ ] Validation:
- Tx подписана master'ом; подпись matches `payload.Owner`.
- `x25519_pub` уникален в registry (не занят другим master'ом).
- `device_name` ≤ 64 символов; printable.
- При `UNLINK_DEVICE`: запись существует, owner совпадает с signer'ом.
- [ ] HTTP: `GET /api/devices/{master_pub}``[{x25519_pub, device_name, added_at}, …]`,
фильтрует revoked.
- [ ] В `/api/identity/{pub}` добавить поле `device_count`.
- [ ] Unit-тесты:
- Happy path link + list.
- Unlink → list сократился.
- Лимит `MaxDevicesPerOwner`.
- Чужая подпись → reject.
- Duplicate x25519_pub → reject.
- [ ] Swagger + `docs/api/devices.md`.
Совместимость: старые клиенты, которые не обновили identity, продолжают
работать по old-schema (envelope на published X25519 из identity). Sender
fall-back'ит на identity.x25519, если `device_count == 0`.
#### PR #2 — Client fan-out (mobile) — _v2.2.0-alpha2_
- [ ] `lib/api.ts`: `fetchDevices(masterPub): Promise<DeviceRecord[]>`,
с кэшем в zustand store + инвалидацией по WS-ивенту `tx:LINK_DEVICE|UNLINK_DEVICE`.
- [ ] `chats/[id].tsx``sendCore`: `Promise.all` по `devices[]`;
если `devices.length == 0` — fall-back на identity.x25519 (legacy path).
- [ ] При первом старте (онбординг): если identity ещё не зарегистрирована —
`PUBLISH_KEY` tx + `LINK_DEVICE` tx (**этот** девайс как первый в списке).
- [ ] UI: в chat-list tile бейдж `2/2` на своих сообщениях (доставлено
на N устройств получателя).
- [ ] Тест: два клиента на одну identity, отправляем на contact — оба
получают.
#### PR #3 — Devices screen + pairing flow (mobile + desktop) — _v2.2.0-alpha3_
- [ ] Settings → **Devices**: список активных, unlink-кнопка у каждого
(кроме `this device`).
- [ ] «Link new device» flow:
- Новое устройство генерит свой X25519, показывает QR
`{x25519_pub, device_name, nonce, rendezvous_id}`.
- Старое сканирует → Confirm → подписывает `LINK_DEVICE` tx +
шлёт через relay envelope `{master_ed25519_priv, recent_history}`
encrypted for new device's X25519, TTL ≤ 60 сек.
- Новое читает envelope, сохраняет master + history, готово.
- [ ] Self-wipe при обнаружении своего `x25519_pub` в revoked:
zeroize master, clear AsyncStorage/keychain, redirect на onboarding.
#### PR #4 — Desktop Electron shell — _v2.2.0-rc1_
См. отдельный раздел ниже.
### Нерешённые вопросы
- **QR-pairing UX на desktop'е**: у ноутбука камеры часто нет. Вариант:
master на phone шлёт invite-envelope по pre-shared secret (6-digit code,
вводится в обе стороны), без QR. Подумать после PR #2.
- **History-sync backup формат**: JSON export всей ChatStorage shreds'ов?
Ограничение по размеру? TTL у backup-конверта? — в PR #3.
- **Revoked-device-wipe race**: если устройство оффлайн 6 месяцев, потом
поднимается — первое что видит это свой revoked. Должен успеть wipe'нуться
**до** попытки послать tx с master-ключом. Продумать порядок bootstrap'а.
---
## Desktop client (Electron)
Цель: 1:1 функциональный паритет с мобильным, десктопная эргономика, keyboard-first.
### Архитектура
- **Electron** + **React (Vite)** + **TypeScript**.
- 80% `lib/` переиспользуется из `client-app/lib/` (api, feed, crypto, store, types).
- Нативные модули: `electron-store` (+ `safeStorage`) для keys, `clipboard`,
`Notification`, `dialog.showOpenDialog`.
- Custom title bar (frame-less), 3-panel layout.
### Экраны
Full design — `docs/desktop-design.md` (TODO). Sections:
- **Messages** — список чатов + conversation panel. Saved Messages pinned top.
- **Feed** — tabs (For You / Following / Trending 24h / 7d / Hashtag) +
post list + thread panel.
- **Wallet** — account overview + tx history + tx detail.
- **Contacts** — grouped list (All / Online / Blocked / Requests) + contact profile.
- **Settings** — grouped: Node, Identity, **Devices**, Privacy, Feed, Notifications,
Advanced, About.
- **Profile** (внизу nav) — self profile; чужой — в detail pane.
- **Auth/Onboarding** — Welcome / Create / Import / Pair-with-phone (QR/code).
### Стилистика (переносим)
Чёрный `#000`, карточки `#0a0a0a`, бордеры `#1f1f1f`, текст `#fff`/`#8b8b8b`.
Синий accent `#1d9bf0`, оранж warning `#f0b35a`. Синий bookmark-avatar для Saved Messages.
### Не переносим
Все React-Native-анимации (Pressable-animations, `scaleY: -1`, gesture-handler
long-press). На десктопе — правый клик, hover-states, keyboard shortcuts,
обычный scroll вместо inverted FlatList.
### Global keybinds
| Key | Action |
|---|---|
| `Ctrl/Cmd+1..5` | Переключение секций |
| `Ctrl/Cmd+N` | Новый пост / новый контакт (контекстно) |
| `Ctrl/Cmd+K` | Global search |
| `Ctrl/Cmd+F` | Search в текущей list pane |
| `Ctrl/Cmd+,` | Settings |
| `Ctrl/Cmd+Enter` | Send (chat / publish post) |
| `Esc` | Close modal / cancel selection |
### Структура папок
```
desktop/
├── electron/
│ ├── main.ts # BrowserWindow, IPC, auto-update
│ ├── preload.ts # contextBridge — safe API
│ ├── menu.ts # native menu bar
│ └── deep-link.ts # dchain:// protocol handler
├── src/ # renderer
│ ├── App.tsx
│ ├── shell/ # nav, status bar, title bar
│ ├── sections/
│ │ ├── messages/
│ │ ├── feed/
│ │ ├── wallet/
│ │ ├── contacts/
│ │ ├── settings/
│ │ └── profile/
│ ├── modals/
│ ├── auth/
│ ├── lib/ # reuse из client-app/lib
│ └── styles/
└── package.json
```
### План работ
- [x] **v2.2.0-alpha4** — Boilerplate: Electron + Vite + React + TS,
frame-less window, 3-panel shell, nav + status bar, safeStorage
for keyfile via IPC, Welcome + Create/Import auth flow, section
stubs that the rest of the alphas will fill in.
- [ ] **v2.2.0-alpha5** — Messages section (chat list + conversation)
using the same fan-out semantics as mobile. Pairing flow wired up
(new-device poll loop + primary-device modal reused from mobile).
- [ ] **v2.2.0-alpha6** — Feed + Wallet real content (reuse feed.ts /
tx builders from client-app via a shared workspace package).
- [ ] **v2.2.0-rc1** — Contacts + Settings → Devices + Profile,
polish pass (keybinds, focus, drag-drop attachments).
- [ ] **v2.2.0** — Auto-update through the same `/api/update-check`
pipeline nodes use; `electron-builder``.dmg`, `.exe`,
`.AppImage`, `.deb`.
### Открытые вопросы (desktop)
- **Auto-update channel**: гнать через Gitea releases (как node) или отдельный S3/Gitea-attachments?
- **Sandbox**: desktop хранит master Ed25519 — обязательно `safeStorage` (macOS Keychain, Windows DPAPI, libsecret на Linux). На Linux без libsecret — fallback на plaintext + warning.
- **Deep-link**: `dchain://chat/<pub>` для шаринга профилей/постов из браузера.
---
## Меньшие хвосты (неприоритетно)
- `prev_hash mismatch` при двойной gossip-доставке — понизить до Debug уровня,
не пугать операторов (см. history с joiner-node bring-up).
- `/api/network-info.peers` у seed'а возвращает `[]` если
`DCHAIN_ANNOUNCE` не задан — добавить fallback:
если announce пустой, публиковать *первый listen-addr + свой peer_id*
(не `127.0.0.1`), чтобы joiner'ы не упирались в `peers:[]`.
- **Groups (3+ участников)** в чатах — тип `Contact.kind == 'group'`
в клиенте есть, а на backend'е не реализован. После v2.2.0, потому что
pairwise messaging ↔ group messaging на multi-device — это другой
математический слой (MLS или Sender Keys).
- Repa как weight в selection-lider PBFT — сейчас только scoring; для
настоящего proof-of-reputation нужно переписать leader-rotation.