F1: warmPageYdoc now returns didSync (true only on the real 'synced' event,
false on the 8s timeout / error, which are now logged). The tree menu gates
the 'available offline' success toast on result.ok AND didSync, and pushes
'editor' into the failed set otherwise — so a page whose Yjs body never
warmed is no longer reported as fully offline-available.
F2: tests for the page/space/currentUser failure labels and the didSync
true/false paths.
F3: correct the mobile-app-plan / mobile-bootstrap present-state sections to
match shipped code (Capacitor, CORS allowlist, Swagger, offline reading,
/l in the SW denylist).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
382 lines
32 KiB
Markdown
382 lines
32 KiB
Markdown
# Мобильное приложение gitmost — исследование и план
|
|
|
|
> Статус: исследовательский + проектный документ.
|
|
> Контекст: gitmost — форк Docmost, чистое веб-приложение. Отдельного
|
|
> мобильного (нативного/устанавливаемого) приложения **нет**.
|
|
> Цель: определить путь к мобильным приложениям — **iOS обязательно, Android
|
|
> как пойдёт**. Оффлайн-чтение уже реализовано (`apps/client/src/features/offline/`,
|
|
> этапы M0…M2 — персист TanStack Query в IndexedDB + Yj/`y-indexeddb` тело
|
|
> документа); полноценная двусторонняя синхронизация (этапы M3…M4) ещё впереди.
|
|
|
|
Документ фиксирует, что уже есть в коде, почему путь к мобилке предопределён
|
|
устройством продукта, сравнивает варианты и описывает рекомендуемый план с
|
|
привязкой к файлам.
|
|
|
|
---
|
|
|
|
## 1. TL;DR
|
|
|
|
1. **Capacitor-обвязка заведена, собранного нативного приложения ещё нет.** В
|
|
монорепо добавлены `@capacitor/core|android|ios|cli` и корневой
|
|
`capacitor.config.ts` (бутстрап этого PR, см. §12). Сгенерированные нативные
|
|
проекты (`ios/`, `android/`) намеренно не хранятся в VCS, и сборки в App
|
|
Store / Play ещё нет — это первый шаг к мобильному клиенту, а не готовое
|
|
приложение.
|
|
2. **Адаптивная веб-версия — есть, и довольно проработанная.** Веб-клиент
|
|
открывается с телефона как mobile-friendly сайт: сворачиваемый сайдбар-drawer,
|
|
отдельные мобильные компоненты (история, поиск, хлебные крошки), responsive-
|
|
примитивы Mantine, mobile-tuned `viewport`. Это готовый фундамент UI.
|
|
3. **Ядро продукта — веб-редактор — нативно не воспроизвести.** TipTap 3
|
|
(ProseMirror) + совместное редактирование на Yjs/Hocuspocus плотно сшиты с
|
|
React. Production-порта Yjs под Swift/Kotlin нет. Любой реалистичный путь
|
|
оставляет редактор в **WebView**.
|
|
4. **API уже готов к нативному клиенту.** Сервер принимает JWT не только из
|
|
cookie, но и из заголовка `Authorization: Bearer`. Есть точка входа для
|
|
вебсокета совместного редактирования (`POST /auth/collab-token`).
|
|
5. **Рекомендуемый путь — Capacitor:** обернуть существующий React-SPA в
|
|
нативную оболочку (iOS + Android из одного кода), добавить нативные плагины
|
|
(push, биометрия, share, файлы). Эволюция в гибрид (нативная навигация +
|
|
WebView-редактор) делается потом инкрементально, без переписывания.
|
|
6. **Оффлайн-чтение уже реализовано** (Yjs + `y-indexeddb` + персист TanStack
|
|
Query в IndexedDB, этапы M0…M2 — см. `apps/client/src/features/offline/`).
|
|
Полная двусторонняя синхронизация записи (этапы M3…M4) ведётся отдельно;
|
|
мобильное приложение этот план переиспользует, а не дублирует.
|
|
7. **Главный блокер — не технический, а лицензионный.** AGPL форка несовместима
|
|
с условиями App Store, если зашивать веб-клиент в бинарник: DRM/usage-rules
|
|
Apple = «дополнительные ограничения», запрещённые AGPLv3 §10. Развязки —
|
|
грузить клиент с сервера (не из `.ipa`), PWA или sideload. Детали и матрица —
|
|
в §9; закрывать **до** кода обёртки.
|
|
|
|
---
|
|
|
|
## 2. Текущее состояние (как есть)
|
|
|
|
### 2.1. Стек
|
|
|
|
| Слой | Технологии |
|
|
|---|---|
|
|
| Бэкенд | NestJS 11 + Fastify, Kysely/Postgres, Redis/BullMQ. API в стиле RPC-POST (соглашение Docmost). Аутентификация — JWT. |
|
|
| Фронт | React 18 + Vite + Mantine + TanStack Query + i18next. Обычный SPA. |
|
|
| Ядро (редактор) | TipTap 3 (ProseMirror) + совместное редактирование на Yjs через Hocuspocus — см. [page-editor.tsx](../apps/client/src/features/editor/page-editor.tsx). |
|
|
| Оффлайн-фундамент | `yjs` + `y-indexeddb` уже в зависимостях клиента (локальная CRDT-копия тела документа). |
|
|
|
|
### 2.2. Capacitor-обвязка заведена (собранного приложения ещё нет)
|
|
|
|
Корневой `package.json` содержит `@capacitor/core|android|ios` (и `@capacitor/cli`
|
|
в devDependencies), в корне лежит [capacitor.config.ts](../capacitor.config.ts).
|
|
`react-native`, `cordova`, `expo` по-прежнему не используются. Сгенерированные
|
|
нативные проекты (`ios/`, `android/`) намеренно не коммитятся — это бутстрап
|
|
оболочки, а не готовый бинарник.
|
|
|
|
### 2.3. Адаптивная веб-версия — есть
|
|
|
|
| Что | Где |
|
|
|---|---|
|
|
| Адаптивная оболочка Mantine `AppShell` с `breakpoint: "sm"`, раздельные состояния `collapsed.mobile` / `collapsed.desktop` | [global-app-shell.tsx](../apps/client/src/components/layouts/global/global-app-shell.tsx) (L85–99) |
|
|
| Отдельный мобильный сайдбар-drawer (`mobileSidebarAtom` отделён от `desktopSidebarAtom`), авто-закрытие при навигации по дереву | [sidebar-atom.ts](../apps/client/src/components/layouts/global/hooks/atoms/sidebar-atom.ts), [space-tree-row.tsx](../apps/client/src/features/page/tree/components/space-tree-row.tsx) (L147–148) |
|
|
| Мобильная модалка истории + свой CSS | [history-modal.tsx](../apps/client/src/features/page-history/components/history-modal.tsx) (L17–19), `history-modal-mobile.tsx` |
|
|
| Мобильный контрол поиска | [search-control.tsx](../apps/client/src/features/search/components/search-control.tsx) (L38–42) |
|
|
| Мобильный рендер хлебных крошек через `useMediaQuery` | [breadcrumb.tsx](../apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx) (L41) |
|
|
| Responsive-примитивы `hiddenFrom`/`visibleFrom` (~16 мест), медиа-запросы в CSS-модулях | по всему `apps/client/src` |
|
|
| Mobile-tuned viewport (`width=device-width, user-scalable=no`) | [index.html](../apps/client/index.html) (L8) |
|
|
|
|
> Важно: адаптив проверялся в мобильном **браузере**, а не в WebView нативной
|
|
> оболочки. Перед сборкой приложения нужно прогнать UI как PWA/в WebView и
|
|
> отловить отличия (жесты, экранная клавиатура/IME в редакторе, safe-area).
|
|
|
|
### 2.4. Готовность API к нативному клиенту
|
|
|
|
- **Bearer-токен уже поддержан.** JWT извлекается из cookie **или** из заголовка
|
|
`Authorization`: см. [jwt.strategy.ts](../apps/server/src/core/auth/strategies/jwt.strategy.ts) (L27–29).
|
|
Серверная сторона нативной авторизации менять не нужно. (Подтверждено
|
|
мобильным бутстрапом.)
|
|
- **Токен можно вернуть в теле логина (opt-in).** [`login`](../apps/server/src/core/auth/auth.controller.ts)
|
|
по-прежнему кладёт JWT в `httpOnly`-cookie, а при флаге `returnToken` дополнительно
|
|
возвращает его в теле ответа (`data.authToken`) для нативных клиентов; веб-клиент
|
|
остаётся на cookie. Реализовано мобильным бутстрапом.
|
|
- **Точка входа вебсокета коллаборации:** [`POST /auth/collab-token`](../apps/server/src/core/auth/auth.controller.ts) (L187–193).
|
|
(Подтверждено мобильным бутстрапом.)
|
|
- **CORS — явный allowlist.** Вместо безусловного `app.enableCors()` теперь
|
|
настраиваемый whitelist через `CORS_ALLOWED_ORIGINS` плюс автоматически
|
|
разрешённые нативные WebView-origin'ы (Capacitor/Ionic/localhost). Реализовано
|
|
мобильным бутстрапом.
|
|
- **OpenAPI/Swagger — опционально.** Swagger UI доступен на `/api/docs` за флагом
|
|
`SWAGGER_ENABLED` (по умолчанию выключен), что даёт авто-генерацию типизированного
|
|
клиента. Реализовано мобильным бутстрапом.
|
|
|
|
---
|
|
|
|
## 3. Почему путь к мобилке предопределён
|
|
|
|
Три факта диктуют решение независимо от моды:
|
|
|
|
1. **Редактор практически невозможно переписать нативно.** ProseMirror + весь
|
|
набор TipTap-расширений + Yjs-CRDT — это не «поле ввода». Нативного
|
|
production-порта Yjs под Swift/Kotlin нет (есть Rust `yrs` с биндингами, но
|
|
это отдельный тяжёлый проект). Переписывание ядра нативно = годы и вечное
|
|
расхождение с веб-версией. **Вывод: редактор остаётся в WebView.**
|
|
2. **API уже умеет нативного клиента** (Bearer, collab-token).
|
|
3. **Оффлайн-фундамент уже заложен** на веб-уровне (Yjs + `y-indexeddb`),
|
|
и он работает внутри WebView.
|
|
|
|
---
|
|
|
|
## 4. Три возможных пути
|
|
|
|
| Путь | Суть | Плюсы | Минусы | Вердикт |
|
|
|---|---|---|---|---|
|
|
| **A. Полностью нативно** (Swift/Kotlin) | Переписать всё, включая редактор и CRDT-синк | Максимально нативный UX | Воспроизвести ProseMirror + расширения + Yjs; несоразмерные трудозатраты; вечное отставание от веба | ❌ Не наш случай |
|
|
| **B. WebView-обёртка SPA (Capacitor)** | Обернуть существующий React-клиент в нативную оболочку, native-возможности — плагинами | Реюз ~100% кода (редактор, коллаборация, оффлайн); один кодовый бэйз → iOS+Android; быстро | Менее «нативно»; риск отказа App Store за «просто сайт» (4.2) — лечится нативной ценностью | ✅ Рекомендуется |
|
|
| **C. Гибрид: нативная оболочка + WebView-редактор** | Навигация/списки/поиск/логин — нативно (React Native/Swift), экран редактирования — web в WebView | Лучший UX; путь Notion/Linear | Заметно больше работы; нужен мост JS↔native | ⚖️ Цель эволюции из B |
|
|
|
|
---
|
|
|
|
## 5. Рекомендуемый путь
|
|
|
|
**B (Capacitor) как первый релиз, с заложенной эволюцией в C.**
|
|
|
|
Почему:
|
|
- Capacitor создан под сценарий «есть веб-приложение → хочу его в App Store с
|
|
нативными возможностями». Переиспользуется весь React-клиент и, главное,
|
|
редактор — то, что нативно не сделать.
|
|
- Один кодовый бэйз закрывает «iOS обязательно» и «Android как пойдёт»
|
|
одновременно, без второй команды.
|
|
- Адаптивная вёрстка уже есть (см. §2.3) — переверстывать под телефон с нуля
|
|
не нужно; работа смещается в нативную обвязку.
|
|
- Оффлайн-будущее подготовлено (Yjs + `y-indexeddb`); оффлайн-синхронизация
|
|
ведётся отдельным планом (этапы M0…M4).
|
|
- Когда упрётесь в UX отдельных экранов — их по одному выносят в нативную
|
|
оболочку, оставив редактор в WebView. То есть B → C делается инкрементально.
|
|
|
|
Почему **не** чистый React Native сразу: редактор всё равно придётся держать в
|
|
WebView (ядро web-only), но при этом теряется прямой реюз остального React-кода
|
|
и появляется мост как обязательная сложность с первого дня — для iOS-first
|
|
старта это лишний оверхед.
|
|
|
|
> Альтернатива: если критичен максимально нативный UX с первого релиза и есть
|
|
> ресурс — сразу путь C на React Native (Expo) с WebView только под редактор.
|
|
> Это сознательный размен «больше работы сейчас» за «более нативное ощущение».
|
|
|
|
⚠️ **Лицензионная оговорка к iOS.** Обычный Capacitor зашивает веб-билд
|
|
`apps/client` в `.ipa` — для публикации в App Store это **нарушает AGPL**
|
|
(см. §9). Выбор Capacitor для **Android** остаётся в силе, но на **iOS**
|
|
веб-клиент нельзя бандлить в бинарник: либо грузить его с сервера
|
|
(`server.url`), либо PWA. То есть рекомендация «B (Capacitor)» применима к
|
|
Android как есть, а к iOS — только в конфигурации без зашитого AGPL.
|
|
|
|
---
|
|
|
|
## 6. Что доработать на бэкенде
|
|
|
|
Часть уже сделана бутстрапом этого PR (см. §2.4), осталось нативно-инфраструктурное:
|
|
|
|
1. **Выдача токена в теле ответа для нативного хранения — ✅ сделано.** Логин
|
|
по-прежнему кладёт JWT в `httpOnly`-cookie, а при opt-in флаге `returnToken`
|
|
дополнительно возвращает токен в теле (`data.authToken`) для нативного хранения
|
|
в Keychain/Keystore и отправки как `Authorization: Bearer`. Сервер принимал
|
|
Bearer и раньше; добавлена именно **выдача**.
|
|
Файлы: [auth.controller.ts](../apps/server/src/core/auth/auth.controller.ts).
|
|
2. **CORS — ✅ сделано.** Безусловный `app.enableCors()` заменён на явный allowlist:
|
|
[`buildCorsAllowlist`/`isOriginAllowed`](../apps/server/src/integrations/environment/cors.util.ts)
|
|
собирают доверенные origin'ы из `CORS_ALLOWED_ORIGINS` плюс нативные
|
|
WebView-origin'ы; [main.ts](../apps/server/src/main.ts) передаёт их в
|
|
`app.enableCors({ origin: callback, credentials: true })`.
|
|
3. **Push-уведомления.** Модуль `notification` уже есть — добавить регистрацию
|
|
device-token и интеграцию **APNs** (iOS) / **FCM** (Android). (Ещё не сделано.)
|
|
4. **OpenAPI/Swagger — ✅ сделано (opt-in).** Подключён `@nestjs/swagger`; Swagger
|
|
UI доступен на `/api/docs` за флагом `SWAGGER_ENABLED` (по умолчанию выключен),
|
|
что даёт авто-генерацию типизированного клиента.
|
|
|
|
---
|
|
|
|
## 7. Android-специфика
|
|
|
|
На пути Capacitor Android едет почти бесплатно (`npx cap add android` из того же
|
|
веб-билда), но есть нюансы:
|
|
|
|
- **Движок в плюс.** Android System WebView (Chromium) обновляется через Play
|
|
Store независимо от ОС и обычно свежее iOS WKWebView. Более рискованный движок
|
|
по совместимости — это iOS, а не Android.
|
|
- **Фрагментация.** Дешёвые/старые устройства с малой памятью и устаревшим
|
|
WebView; стек тяжёлый (ProseMirror + Yjs + mermaid + katex + excalidraw) —
|
|
тестировать на бюджетных аппаратах.
|
|
- **Обвязка под Android:** аппаратная/жестовая кнопка «Назад» (навигация внутри
|
|
приложения, а не выход), **FCM** для push, Android App Links (вместо iOS
|
|
Universal Links), подписание и Play Console.
|
|
- **Главный риск именно для Android — ввод текста в ProseMirror на Gboard/IME.**
|
|
Историческая боль `contenteditable` на Android (прыжки курсора, дубли символов
|
|
при композиции). Стало лучше, но **проверять в первую очередь и рано**.
|
|
- **Магазин.** Google Play лояльнее к webview-обёрткам, чем App Store; риск
|
|
«отклонят как просто сайт» для Play практически неактуален.
|
|
|
|
---
|
|
|
|
## 8. iOS-специфика
|
|
|
|
- **WKWebView** на движке WebKit жёстко привязан к версии ОС — это более
|
|
рискованный по совместимости движок (тестировать прежде всего его).
|
|
- **App Store guideline 4.2 (minimum functionality).** Чистая webview-обёртка
|
|
рискует отклонением «это просто сайт». Лечится реальной нативной ценностью:
|
|
push, share-extension, биометрический разблок, оффлайн-кэш — всё это Capacitor
|
|
даёт плагинами.
|
|
- **safe-area** под «чёлку»/системные панели, поведение экранной клавиатуры в
|
|
редакторе.
|
|
|
|
---
|
|
|
|
## 9. Лицензионный блокер: AGPL ↔ App Store (iOS)
|
|
|
|
> Это не инженерная, а **лицензионная** задача — закрывать её надо **до** кода
|
|
> обёртки, иначе можно сделать приложение, которое некуда легально опубликовать.
|
|
> Ниже — инженерно-лицензионный разбор, **не** юридическая консультация; финально
|
|
> подтверждать у того, кто разбирается в лицензиях.
|
|
|
|
### 9.1. Суть конфликта
|
|
|
|
gitmost — форк Docmost под **AGPL-3.0** (константа форка: «100% open, AGPL-only»).
|
|
Две вещи несовместимы:
|
|
|
|
- **AGPLv3 §10** (последний абзац) запрещает накладывать на получателя кода
|
|
**любые дополнительные ограничения** сверх самой лицензии.
|
|
- **Стандартный EULA App Store** ровно их и накладывает: **FairPlay/DRM**,
|
|
привязка установки к Apple ID с лимитом устройств (**usage rules**), запрет
|
|
свободного перераспространения бинарника.
|
|
|
|
Приняв условия Apple, чтобы попасть в App Store, вы нарушаете AGPL кода, который
|
|
раздаёте.
|
|
|
|
### 9.2. Почему это бьёт именно по форку
|
|
|
|
Запрет «дополнительных ограничений» связывает **лицензиатов, но не самого
|
|
правообладателя**: владелец 100% копирайта может опубликовать свой код в App Store.
|
|
Но в gitmost бóльшая часть копирайта принадлежит **upstream-Docmost** и
|
|
контрибьюторам — вы выступаете дистрибьютором *чужого* AGPL-кода и не можете
|
|
единолично добавить App-Store-исключение.
|
|
|
|
Прецеденты: **VLC** (удалён из App Store в 2011 по жалобе на конфликт GPL с
|
|
условиями стора; вернулся только после перелицензирования и согласия
|
|
правообладателей), **GNU Go** — снят по той же причине. Это не теоретический риск.
|
|
|
|
### 9.3. Ключевой принцип развязки: лицензия смотрит на `.ipa`, а не на устройство
|
|
|
|
Определяющее — **что раздаёт сам Apple** (`.ipa` под FairPlay) и **кто раздаёт
|
|
AGPL-байты**, а не то, окажутся ли они в итоге на устройстве:
|
|
|
|
- AGPL **внутри `.ipa`** → получен под ограничениями Apple → **нарушение**.
|
|
- AGPL **скачан с вашего сервера** → получен от вас под AGPL (исходники открыты,
|
|
§13 выполнен) → ограничения Apple на него **не** накладываются, даже если бандл
|
|
кэшируется в песочнице приложения.
|
|
|
|
Следствие: **офлайн на iOS легально достижим** — если кэшированный бандл пришёл с
|
|
вашего сервера, а не из `.ipa`. Ограничение тут не лицензионное, а в **ревью
|
|
Apple** (см. §9.5).
|
|
|
|
### 9.4. Варианты «грузить веб-клиент с сервера»
|
|
|
|
**A. WebView навигируется на хостед-клиент (`server.url`).** Capacitor умеет
|
|
`server: { url: 'https://app.example.com' }` — оболочка грузит WebView с удалённого
|
|
URL, мост и нативные плагины по-прежнему инжектятся. В `.ipa` — ноль AGPL.
|
|
|
|
- Плюс: лицензионно самый чистый; **origin = ваш домен**, поэтому cookie/CORS
|
|
работают как в браузере (боль `capacitor://localhost` ↔ API из §6 исчезает —
|
|
токен в body/Keychain может и не понадобиться).
|
|
- Минус: холодный старт требует сети; сервер лёг → приложение кирпич; офлайна по
|
|
умолчанию нет.
|
|
|
|
**B. OTA: пустой шелл скачивает и кэширует бандл.** Шелл при первом запуске тянет
|
|
JS-бандл с вашего сервера и кэширует как веб-ассеты (механизм Cordova/CodePush).
|
|
Open-source self-host-вариант — `@capgo/capacitor-updater` (важно для AGPL-проекта:
|
|
без привязки к проприетарному Appflow).
|
|
|
|
- Плюс: **даёт офлайн** — кэш AGPL легален, т.к. распространён вами, а не Apple.
|
|
- Минус: упирается в политику Apple по hot-update (§9.5).
|
|
|
|
**Не-обходы (мифы):** «никто не засудит» — это нарушение, а не обход; «LGPL-нуть
|
|
обёртку» — не помогает (проблема в AGPL-веб-клиенте, а не в обёртке); «mere
|
|
aggregation» — не катит: зашитый бандл это комбинированное распространяемое
|
|
произведение, а не простая агрегация.
|
|
|
|
### 9.5. Гейты Apple
|
|
|
|
| # | Guideline | Суть | Влияние |
|
|
|---|---|---|---|
|
|
| 1 | **2.5.2** (исполняемый код) | Скачивать/исполнять **нативный** код нельзя, **но** есть исключение для скриптов, исполняемых встроенным WebKit/JavascriptCore, если они не меняют назначение приложения | Загрузка веб-клиента в `WKWebView` под исключение попадает: вариант A — чистый, B — терпимый, но с границами |
|
|
| 2 | **4.2** (minimum functionality) | Чистый WebView-«просто сайт» рискует отклонением | Лечится нативной ценностью в оболочке (push/APNs, биометрия, share, файлы — ваш нативный код, не AGPL) |
|
|
| 3 | конфликт двух гейтов | «Лицензионно чистый» вариант (пустой шелл качает всё) — самый рискованный для ревью; «безопасный для ревью» (зашить веб-билд в `.ipa`) — лицензионное нарушение | **Совместить (офлайн) + (чистая AGPL) + (низкий риск ревью) в одной конфигурации нельзя — выбираете любые два** |
|
|
|
|
Безопасность: раз исполняете удалённый код — только HTTPS, желательно cert-pinning
|
|
(подмена сервера = произвольный JS в WebView пользователя).
|
|
|
|
### 9.6. Итоговая матрица распространения iOS
|
|
|
|
| Конфигурация | AGPL-чистота | Офлайн | Риск ревью Apple |
|
|
|---|---|---|---|
|
|
| A. `server.url` на хостед-клиент | ✅ чистая | ❌ нет | средний (4.2, лечится плагинами) |
|
|
| B. OTA пустой шелл + кэш бандла | ✅ чистая | ✅ есть | выше (2.5.2 + 4.2) |
|
|
| Зашить веб-билд в `.ipa` (обычный Capacitor) | ❌ нарушение | ✅ | низкий |
|
|
| **PWA** | ✅ чистая | ✅ | App Store не нужен |
|
|
| Sideload / EU DMA-маркетплейсы (iOS 17.4+) | ✅ чистая | ✅ | вне App Store; **только ЕС** |
|
|
|
|
**Вывод:** для iOS **PWA** — самое дешёвое решение, закрывающее всё сразу. Если
|
|
присутствие именно в App Store критично — **вариант A** (`server.url` + нативные
|
|
плагины под 4.2) легальный и реалистичный ценой «онлайн для холодного старта».
|
|
Офлайн в App Store (вариант B) технически и лицензионно возможен, но это
|
|
максимальный риск на ревью — закладывать только если офлайн на iOS обязателен.
|
|
Совместить «App Store + зашитый офлайн AGPL» легально нельзя, пока копирайт не ваш.
|
|
|
|
---
|
|
|
|
## 10. Оффлайн в будущем
|
|
|
|
Оффлайн-**чтение** уже реализовано (этапы M0…M2 этого PR), позиция хорошая:
|
|
|
|
- Тело документа уже редактируется через Yjs (CRDT) + `y-indexeddb` — локальная
|
|
копия и автослияние правок работают, в том числе в WebView.
|
|
- Чтение «вокруг тела» (навигация, заголовки, комментарии, дерево, текущий
|
|
пользователь) теперь читается оффлайн из персистентного кэша TanStack Query в
|
|
IndexedDB; см. `apps/client/src/features/offline/` (в т.ч. `make-offline.ts` —
|
|
ручной прогрев страницы в оффлайн). Полная **двусторонняя** синхронизация
|
|
записи (этапы M3…M4) ещё впереди.
|
|
- Мобильное приложение **переиспользует** этот план, а не строит оффлайн заново.
|
|
Нюанс Android: System WebView под нехваткой места может чистить хранилище →
|
|
для оффлайна, возможно, понадобится дублировать критичные данные в нативное
|
|
хранилище, чтобы локальные копии не вычищались.
|
|
|
|
---
|
|
|
|
## 11. Открытые вопросы (зафиксировать до старта)
|
|
|
|
- **Q1.** Путь: Capacitor (B) с эволюцией в гибрид, или сразу React Native (C)?
|
|
Рекомендация — B.
|
|
- **Q2.** Мобильная авторизация: отдельный логин-флоу с токеном в body + Keychain/
|
|
Keystore + Bearer (рекомендуется) или попытка работать через cookie в WebView?
|
|
- **Q3.** Push: APNs + FCM сразу или iOS-first?
|
|
- **Q4.** Подключать ли OpenAPI/Swagger для генерации мобильного клиента?
|
|
- **Q5.** Когда включать оффлайн (этапы M0…M4) относительно
|
|
первого мобильного релиза?
|
|
- **Q6.** iOS-дистрибуция при AGPL (§9): App Store через `server.url`
|
|
(онлайн-клиент, без зашитого AGPL), PWA или sideload/EU-маркетплейсы? Этот
|
|
лицензионный путь нужно подтвердить **до** кода обёртки. Рекомендация — PWA для
|
|
iOS, Capacitor для Android.
|
|
|
|
---
|
|
|
|
## 12. Чеклист первого шага (бутстрап Capacitor, iOS-first)
|
|
|
|
- [ ] **Закрыть лицензионный путь iOS (§9) ДО кода обёртки:** выбрать
|
|
`server.url` / PWA / sideload и подтвердить у разбирающегося в лицензиях.
|
|
- [ ] **Не бандлить AGPL-веб-клиент в iOS `.ipa`** (DRM/usage-rules App Store ⟂
|
|
AGPLv3 §10) — на iOS грузить клиент с сервера или идти через PWA.
|
|
- [ ] Прогнать существующий адаптивный UI как PWA/в WebView, отловить отличия
|
|
(жесты, IME в редакторе, safe-area).
|
|
- [x] Добавить Capacitor в монорепо, нацелить на веб-билд `apps/client`
|
|
(Android — зашитый билд; iOS — `server.url`/PWA без зашитого AGPL, см. §9).
|
|
- [ ] `npx cap add ios` (Android — `npx cap add android`, когда будет готова обвязка) — нативные проекты генерируются локально и намеренно не хранятся в VCS (см. §9).
|
|
- [x] Бэкенд: мобильный логин-флоу с токеном в body; хранить токен в Keychain/
|
|
Keystore; слать `Authorization: Bearer`.
|
|
- [x] Бэкенд: явный CORS-whitelist под мобильные origin'ы.
|
|
- [ ] Native-плагины под App Store 4.2: push, биометрия, share, файлы.
|
|
- [ ] Push: APNs (iOS); FCM добавить вместе с Android.
|
|
- [ ] Проверить вебсокет коллаборации из WebView (`/auth/collab-token` + Hocuspocus).
|
|
- [x] (Опционально) Подключить `@nestjs/swagger`.
|