From 4c50036172c6cb2a410621449646c2f3a154a8e2 Mon Sep 17 00:00:00 2001 From: AlexIves16 Date: Tue, 21 Apr 2026 15:54:20 +0500 Subject: [PATCH] Clean up: move docs to Obsidian vault, remove outdated md files --- ADD_SHA1.md | 50 --- ARCHITECTURE.md | 284 --------------- DEEPSEEK_SETUP.md | 246 ------------- DEPLOY_TO_EXISTING_SERVER.md | 194 ---------- FIREBASE_DONE.md | 190 ---------- FIREBASE_SETUP.md | 24 -- GITHUB_SETUP.md | 349 ------------------ MOCK_VS_REAL_API.md | 162 --------- PROJECT_STRUCTURE.md | 208 ----------- QUICKSTART.md | 340 ------------------ SETUP_ADMINS.md | 65 ---- SETUP_COMPLETE.md | 224 ------------ lib/core/gitea_sync_service.dart | 321 +++++++++++++++++ lib/core/markdown_mindmap_parser.dart | 194 ++++++++++ lib/core/offline_mindmap_repository.dart | 180 ++++++++++ lib/core/router.dart | 12 + lib/main.dart | 6 + lib/providers/gitea_sync_provider.dart | 228 ++++++++++++ lib/screens/gitea_mindmaps_list_screen.dart | 151 ++++++++ lib/screens/gitea_sync_settings_screen.dart | 203 +++++++++++ server/DEPLOY.md | 369 -------------------- server/INTEGRATION_GUIDE.md | 164 --------- 22 files changed, 1295 insertions(+), 2869 deletions(-) delete mode 100644 ADD_SHA1.md delete mode 100644 ARCHITECTURE.md delete mode 100644 DEEPSEEK_SETUP.md delete mode 100644 DEPLOY_TO_EXISTING_SERVER.md delete mode 100644 FIREBASE_DONE.md delete mode 100644 FIREBASE_SETUP.md delete mode 100644 GITHUB_SETUP.md delete mode 100644 MOCK_VS_REAL_API.md delete mode 100644 PROJECT_STRUCTURE.md delete mode 100644 QUICKSTART.md delete mode 100644 SETUP_ADMINS.md delete mode 100644 SETUP_COMPLETE.md create mode 100644 lib/core/gitea_sync_service.dart create mode 100644 lib/core/markdown_mindmap_parser.dart create mode 100644 lib/core/offline_mindmap_repository.dart create mode 100644 lib/providers/gitea_sync_provider.dart create mode 100644 lib/screens/gitea_mindmaps_list_screen.dart create mode 100644 lib/screens/gitea_sync_settings_screen.dart delete mode 100644 server/DEPLOY.md delete mode 100644 server/INTEGRATION_GUIDE.md diff --git a/ADD_SHA1.md b/ADD_SHA1.md deleted file mode 100644 index aaa7833..0000000 --- a/ADD_SHA1.md +++ /dev/null @@ -1,50 +0,0 @@ -# 🔑 Как добавить SHA-1 fingerprint - -## Способ 1: Через Android Studio (проще) - -1. Открой проект в Android Studio -2. Справа открой **Gradle** панель -3. Разверни `:android` → `Tasks` → `android` -4. Дважды кликни на **signingReport** -5. В консоли найди строку `SHA1:` -6. Скопируй значение - -## Способ 2: Через командную строку - -```bash -cd c:\Users\ormix\NANAXE\android -.\gradlew app:signingReport -``` - -Ищи строку вида: -``` -SHA1: A1:B2:C3:D4:E5:F6:... -``` - -## Способ 3: Через keytool (если есть Java) - -```bash -keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android -``` - ---- - -## ➕ Добавить в Firebase Console - -1. Открой https://console.firebase.google.com/project/nanax26-1f33b/settings/general/android:com.nanaxe.nanaxe_crm -2. Прокрути вниз до **SHA certificate fingerprints** -3. Нажми **Add fingerprint** -4. Вставь скопированный SHA-1 -5. Сохрани - ---- - -## ✅ Проверка - -После добавления SHA-1: -1. Перескачай `google-services.json` из Firebase Console -2. Замени файл в `android/app/google-services.json` -3. Запусти приложение: `flutter run` -4. Попробуй войти через Google - -Если работает — всё настроено правильно! 🎉 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md deleted file mode 100644 index 3284d40..0000000 --- a/ARCHITECTURE.md +++ /dev/null @@ -1,284 +0,0 @@ -# 🏗 Архитектура NANAXE CRM - -## Общая схема - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Flutter Application │ -├─────────────────────────────────────────────────────────────────┤ -│ Presentation Layer (UI) │ -│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │ -│ │ Screens │ Widgets │ Themes │ Navigation │ │ -│ │ │ │ │ (GoRouter) │ │ -│ └─────────────┴─────────────┴─────────────┴─────────────┘ │ -├─────────────────────────────────────────────────────────────────┤ -│ State Management Layer (Riverpod) │ -│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │ -│ │ Providers │ Notifiers │ Streams │ State │ │ -│ └─────────────┴─────────────┴─────────────┴─────────────┘ │ -├─────────────────────────────────────────────────────────────────┤ -│ Domain Layer (Business Logic) │ -│ ┌─────────────┬─────────────┬─────────────┬─────────────┐ │ -│ │ Models │ Repositories │ Services │ Use Cases │ │ -│ └─────────────┴─────────────┴─────────────┴─────────────┘ │ -├─────────────────────────────────────────────────────────────────┤ -│ Data Layer │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Local Storage (Hive) │ │ -│ │ ┌──────────┬──────────┬──────────┬──────────┐ │ │ -│ │ │ Tasks │ Alarms │ MindMaps │ Events │ │ │ -│ │ └──────────┴──────────┴──────────┴──────────┘ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ ┌─────────────────────────────────────────────────────────┐ │ -│ │ Remote Storage (Firebase Firestore) │ │ -│ └─────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ - ┌────────────────────┼────────────────────┐ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ Firebase Auth │ │ AI Services │ │ Google Calendar │ -│ (Google Sign- │ │ (OpenAI, │ │ API │ -│ In, Email) │ │ Anthropic) │ │ │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -``` - -## Поток данных - -### 1. Аутентификация -``` -User → AuthScreen → AuthNotifier (Riverpod) → Firebase Auth - ↓ - UserModel → Firestore - ↓ - HomeScreen -``` - -### 2. Управление задачами -``` -User → TasksScreen → TaskNotifier → FirestoreService → Firestore - ↓ - LocalStorageService (Hive) - ↓ - Offline Cache -``` - -### 3. AI генерация -``` -User → TaskScreen → AIService → OpenAI API - ↓ - Generate Subtasks - ↓ - TaskModel → Firestore -``` - -### 4. Mind-карта -``` -User → MindMapScreen → MindMapNotifier → FirestoreService - ↓ - MindMap Nodes (Tree) - ↓ - GraphView Widget (Render) -``` - -### 5. Синхронизация -``` -┌──────────────┐ Compare ┌──────────────┐ -│ Local Hive │ ←───────────→ │ Firestore │ -│ (Offline) │ │ (Remote) │ -└──────────────┘ └──────────────┘ - ↓ ↓ - Merge Conflicts Real-time Sync - ↓ ↓ - Local Priority Remote Priority - └──────────────┬──────────────┘ - ↓ - Merged Data → UI -``` - -## Модульная структура - -### Core Module -``` -core/ -├── firebase_config.dart # Инициализация Firebase -├── firestore_service.dart # CRUD операции Firestore -├── local_storage_service.dart # Hive кэш -├── ai_service.dart # AI интеграции -├── router.dart # GoRouter маршруты -└── theme.dart # Material темы -``` - -### Features Modules -``` -features/ -├── auth/ -│ ├── auth_screen.dart -│ ├── auth_provider.dart -│ └── auth_service.dart -├── tasks/ -│ ├── task_list_screen.dart -│ ├── task_detail_screen.dart -│ ├── task_provider.dart -│ └── task_service.dart -├── mindmap/ -│ ├── mindmap_screen.dart -│ ├── mindmap_widget.dart -│ ├── mindmap_provider.dart -│ └── mindmap_service.dart -├── calendar/ -│ ├── calendar_screen.dart -│ ├── event_provider.dart -│ └── google_calendar_service.dart -└── alarms/ - ├── alarms_screen.dart - ├── alarm_provider.dart - └── notification_service.dart -``` - -## Ролевая модель - -``` -┌────────────────────────────────────────────┐ -│ User Roles │ -├──────────────┬──────────────┬──────────────┤ -│ Admin │ Member │ Viewer │ -├──────────────┼──────────────┼──────────────┤ -│ ✓ Все права │ ✓ Создание │ ✓ Просмотр │ -│ ✓ Редактиро- │ задач │ задач │ -│ вание mind-│ ✓ Редактиро- │ ✓ Просмотр │ -│ карт │ вание │ mind-карт │ -│ ✓ Управление │ своих задач│ ✗ Редактиро- │ -│ командой │ ✓ Просмотр │ вание │ -│ ✓ Удаление │ общих задач│ ✗ Удаление │ -│ любых задач│ ✗ Редактиро- │ │ -│ │ вание mind-│ │ -│ │ карт │ │ -└──────────────┴──────────────┴──────────────┘ -``` - -## Модель данных - -### Task Model -```dart -TaskModel { - String id; - String title; - String? description; - String projectId; - String? parentTaskId; // Для иерархии - TaskStatus status; // todo, inProgress, review, done - TaskPriority priority; // low, medium, high, urgent - String? assigneeId; - String? creatorId; - List tags; - DateTime? dueDate; - int estimatedHours; - Map aiMetadata; // AI данные - DateTime createdAt; - DateTime updatedAt; -} -``` - -### MindMapNode Model -```dart -MindMapNode { - String id; - String projectId; - String? parentId; // Для дерева - int level; // Уровень вложенности - String title; - String? taskId; // Связь с задачей - String color; - double x, y; // Координаты - DateTime createdAt; - DateTime updatedAt; -} -``` - -## Синхронизация - -### Стратегия -1. **Offline-first**: Все данные сначала сохраняются локально -2. **Background sync**: Периодическая синхронизация с Firestore -3. **Conflict resolution**: - - Last write wins для простых полей - - Merge для коллекций - - User prompt для критических конфликтов - -### Flow -``` -┌─────────┐ ┌─────────────┐ ┌──────────┐ -│ UI │────→│ Repository │────→│ Firestore│ -└─────────┘ └─────────────┘ └──────────┘ - ↓ ↓ -┌─────────┐ ┌─────────────┐ -│ Cache │←────│ Hive │ -└─────────┘ └─────────────┘ -``` - -## Безопасность - -### Firebase Security Rules -```javascript -// Проверка прав доступа -function hasRole(allowedRoles) { - return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role in allowedRoles; -} - -// Проверка владения -function isOwner(resource) { - return resource.ownerId == request.auth.uid; -} - -// Проверка членства в команде -function isTeamMember(teamId) { - let team = get(/databases/$(database)/documents/teams/$(teamId)).data; - return team.memberIds.hasAny([request.auth.uid]); -} -``` - -## Производительность - -### Оптимизация -1. **Pagination**: Загрузка задач порциями по 50 -2. **Lazy loading**: Mind-карта загружается по частям -3. **Caching**: Hive для офлайн-доступа -4. **Indexes**: Firestore индексы для частых запросов -5. **Batching**: Пакетная запись в Firestore - -### Firestore Indexes -``` -tasks: [projectId, createdAt] [assigneeId, status] [dueDate] -events: [startTime, endTime] [attendeeIds] -mindmaps: [projectId, level] -``` - -## Масштабирование - -### Этапы роста - -**Phase 1: MVP (сейчас)** -- Базовые CRUD операции -- Простая аутентификация -- Локальный кэш - -**Phase 2: Team Features** -- Командная работа -- Роли и разрешения -- Real-time collaboration - -**Phase 3: AI Integration** -- Умное планирование -- Авто-приоритизация -- Генерация отчётов - -**Phase 4: Enterprise** -- SSO интеграция -- Audit logs -- Advanced analytics - ---- - -**Документ создан для команды NANAXE** 🚀 diff --git a/DEEPSEEK_SETUP.md b/DEEPSEEK_SETUP.md deleted file mode 100644 index 0649fdd..0000000 --- a/DEEPSEEK_SETUP.md +++ /dev/null @@ -1,246 +0,0 @@ -# 🤖 DeepSeek AI - Настройка - -## Что такое DeepSeek? - -**DeepSeek** - это мощная китайская нейросеть с открытым API, которая: -- 🚀 По качеству на уровне GPT-4 -- 💰 В 10-20 раз дешевле OpenAI -- 🌐 Поддерживает русский язык -- 🔑 Даёт бесплатные токены при регистрации - ---- - -## 📝 Как получить API ключ - -### Шаг 1: Регистрация - -1. Перейди на https://platform.deepseek.com -2. Нажми **Sign Up** или **Login** -3. Зарегистрируйся через email или Google - -### Шаг 2: Получение API ключа - -1. После входа перейди в **API Keys**: https://platform.deepseek.com/api_keys -2. Нажми **Create API Key** -3. Скопируй ключ (начинается с `sk-`) - -**Важно:** Сохрани ключ в надёжном месте! Показать его можно только один раз. - -### Шаг 3: Пополнение баланса (опционально) - -DeepSeek даёт **бесплатные токены** новым пользователям (~$2-5). - -Для пополнения: -1. Перейди в **Billing**: https://platform.deepseek.com/billing -2. Выбери способ оплаты -3. Минимальное пополнение: ~$10 - ---- - -## 💰 Тарифы - -| Модель | Цена (за 1M токенов) | -|--------|---------------------| -| **DeepSeek Chat** | ~$0.14 (input) / ~$0.28 (output) | -| **DeepSeek Coder** | ~$0.14 (input) / ~$0.28 (output) | - -**Для сравнения:** -- GPT-4: ~$30 / $60 за 1M токенов -- DeepSeek: ~$0.14 / $0.28 за 1M токенов - -**Экономия: в 200+ раз!** 💰 - ---- - -## 🔧 Настройка в проекте - -### 1. Создай файл `.env` - -Скопируй `.env.example` в `.env`: -```bash -cp .env.example .env -``` - -### 2. Добавь API ключ - -Открой `.env` и вставь ключ: -``` -DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx -``` - -### 3. Обновите `lib/core/deepseek_service.dart` - -```dart -final deepSeekService = DeepSeekService( - apiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxxxx', // Твой ключ -); -``` - -Или используй пакет `flutter_dotenv`: - -```dart -// В main.dart -import 'package:flutter_dotenv/flutter_dotenv.dart'; - -Future main() async { - await dotenv.load(fileName: ".env"); - - final deepSeekService = DeepSeekService( - apiKey: dotenv.env['DEEPSEEK_API_KEY'], - ); - - runApp(...); -} -``` - ---- - -## 📱 Примеры использования - -### Генерация подзадач - -```dart -import 'package:nanaxe_crm/core/deepseek_service.dart'; - -final subtasks = await deepSeekService.generateSubtasks( - title: 'Создать CRM систему', - description: 'Разработка CRM для управления задачами команды с AI', -); - -print(subtasks); -// [ -// {"title": "Анализ требований", "estimatedHours": 2, "description": "..."}, -// {"title": "Проектирование", "estimatedHours": 3, "description": "..."}, -// ... -// ] -``` - -### Приоритизация задачи - -```dart -final priority = await deepSeekService.prioritizeTask( - title: 'Срочный баг', - description: 'Пользователи не могут войти', - dueDate: DateTime.now().add(Duration(hours: 2)), -); - -print(priority); // TaskPriority.urgent -``` - -### Умное планирование - -```dart -final schedule = await deepSeekService.smartSchedule( - tasks: myTasks, - startDate: DateTime.now(), - endDate: DateTime.now().add(Duration(days: 7)), - availableHoursPerDay: 6, -); - -print(schedule); -// [ -// {"taskId": 0, "date": "2026-03-27", "hours": 3}, -// {"taskId": 1, "date": "2026-03-27", "hours": 3}, -// ... -// ] -``` - -### Генерация mind-карты проекта - -```dart -final mindMapNodes = await deepSeekService.generateMindMap( - projectName: 'CRM App', - description: 'Мобильное CRM приложение с AI планированием', -); - -print(mindMapNodes); -// [ -// {"level": 0, "title": "CRM App", "parentId": null}, -// {"level": 1, "title": "Планирование", "parentId": 0}, -// {"level": 1, "title": "Разработка", "parentId": 0}, -// ... -// ] -``` - ---- - -## 🆘 Troubleshooting - -### Ошибка: "Invalid API key" - -``` -1. Проверь что ключ начинается с 'sk-' -2. Убедись что нет лишних пробелов -3. Проверь что ключ активен в Dashboard -``` - -### Ошибка: "Insufficient balance" - -``` -1. Проверь баланс в Billing: https://platform.deepseek.com/billing -2. Пополни счёт -3. Или используй mock данные (по умолчанию) -``` - -### Ошибка: "Rate limit exceeded" - -``` -DeepSeek ограничивает запросы. Подожди немного или увеличь лимиты в настройках. -``` - ---- - -## 📊 Статистика использования - -Для отслеживания расходов: - -1. Перейди в **Usage**: https://platform.deepseek.com/usage -2. Просматривай статистику по дням/моделям -3. Настрой алерты при достижении лимита - ---- - -## 🔒 Безопасность - -**Никогда не публикуй API ключ в GitHub!** - -✅ Правильно: -```bash -# Добавить .env в .gitignore -echo ".env" >> .gitignore -``` - -✅ Использовать в коде: -```dart -// Загружать из .env файла -final apiKey = dotenv.env['DEEPSEEK_API_KEY']; -``` - -❌ Неправильно: -```dart -// Хардкод ключа в коде -final apiKey = 'sk-xxxxx'; // НЕ ДЕЛАЙ ТАК! -``` - ---- - -## 📚 Ресурсы - -- **Официальная документация**: https://platform.deepseek.com/docs -- **API Reference**: https://platform.deepseek.com/api-docs -- **Цены**: https://platform.deepseek.com/pricing -- **Dashboard**: https://platform.deepseek.com - ---- - -## 🎯 Следующие шаги - -1. ✅ Получить API ключ -2. ✅ Добавить в `.env` -3. ✅ Протестировать генерацию подзадач -4. ✅ Интегрировать в UI приложения -5. ✅ Настроить отслеживание расходов - ---- - -**DeepSeek готов к работе! 🚀** diff --git a/DEPLOY_TO_EXISTING_SERVER.md b/DEPLOY_TO_EXISTING_SERVER.md deleted file mode 100644 index da7926b..0000000 --- a/DEPLOY_TO_EXISTING_SERVER.md +++ /dev/null @@ -1,194 +0,0 @@ -# 🚀 Деплой Nanaxe Server на существующий Docker сервер - -Если у вас уже работает другой проект на Docker, Nanaxe Server можно развернуть на других портах. - -## 📋 Маппинг портов - -| Сервис | Внутренний порт | Внешний порт | Описание | -|--------|----------------|--------------|----------| -| Gitea | 3000 | **3001** | Git сервер | -| Gitea SSH | 22 | **2221** | Git SSH | -| PostgreSQL | 5432 | **5433** | База данных | -| Nanaxe API | 8080 | **8082** | REST API | -| Nginx | 80 | **8083** | Web UI | -| Nginx HTTPS | 443 | **8444** | Web UI SSL | -| WebDAV | 80 | **8084** | Файловый доступ | - -## 🚀 Быстрый старт - -### 1. На сервере создайте директорию - -```bash -ssh root@YOUR_SERVER_IP - -mkdir -p /opt/nanaxe-server -cd /opt/nanaxe-server -``` - -### 2. Скопируйте файлы - -```bash -# Клонируйте репозиторий -git clone https://github.com/YOUR_USERNAME/NANAXE.git temp -cp -r temp/server/* . -rm -rf temp -``` - -### 3. Настройте окружение - -```bash -cp .env.example .env -nano .env -``` - -Заполните: -```env -GITEA_DOMAIN=your-domain.com -JWT_SECRET=your-secret-key -POSTGRES_PASSWORD=your-db-password -WEBDAV_USER=admin -WEBDAV_PASSWORD=your-password -``` - -### 4. Запустите - -```bash -docker compose up -d -``` - -### 5. Проверьте - -```bash -# Проверьте статус -docker compose ps - -# Проверьте логи -docker compose logs -f - -# Проверьте API -curl http://localhost:8082/health - -# Откройте Gitea -open http://YOUR_SERVER_IP:3001 -``` - -## 🔧 Настройка Nginx Reverse Proxy (опционально) - -Если у вас уже есть Nginx для другого проекта, добавьте: - -```nginx -# /etc/nginx/sites-available/nanaxe - -server { - listen 80; - server_name nanaxe.yourdomain.com; - - location / { - proxy_pass http://localhost:8083; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} - -server { - listen 80; - server_name git.yourdomain.com; - - location / { - proxy_pass http://localhost:3001; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} - -server { - listen 80; - server_name api.yourdomain.com; - - location / { - proxy_pass http://localhost:8082; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} -``` - -```bash -ln -s /etc/nginx/sites-available/nanaxe /etc/nginx/sites-enabled/ -nginx -t -systemctl reload nginx -``` - -## 🔄 GitHub Actions деплой - -### 1. Добавьте Secrets - -В GitHub репозитории: **Settings → Secrets → Actions** - -``` -NANAXE_SSH_KEY=-----BEGIN OPENSSH PRIVATE KEY----- - (приватный ключ) - -----END OPENSSH PRIVATE KEY----- -NANAXE_HOST=YOUR_SERVER_IP -NANAXE_USER=root -NANAXE_PATH=/opt/nanaxe-server -``` - -### 2. Workflow уже настроен - -При push в `master` ветку: -1. Собирается Docker образ -2. Публикуется в GitHub Container Registry -3. Деплоится на ваш сервер -4. Перезапускаются контейнеры - -## 🧪 Локальная разработка - -Для локального тестирования с стандартными портами: - -```bash -cd server -docker compose -f docker-compose.yml -f docker-compose.override.yml up -d -``` - -## 📱 Доступ к сервисам - -После деплоя: - -- **Gitea**: http://YOUR_SERVER_IP:3001 -- **API**: http://YOUR_SERVER_IP:8082 -- **Web UI**: http://YOUR_SERVER_IP:8083 -- **WebDAV**: http://YOUR_SERVER_IP:8084 - -## 🐛 Устранение неполадок - -### Порт уже занят - -```bash -# Найдите процесс -netstat -tlnp | grep 3001 - -# Или измените порт в docker-compose.yml -``` - -### Конфликт имён контейнеров - -```bash -# Проверьте запущенные контейнеры -docker ps - -# Если есть конфликт, измените имена в docker-compose.yml -``` - -### Проблемы с сетью - -```bash -# Пересоздайте сеть -docker compose down -docker network prune -docker compose up -d -``` - -## ✅ Готово! - -Nanaxe Server работает параллельно с вашим другим проектом! diff --git a/FIREBASE_DONE.md b/FIREBASE_DONE.md deleted file mode 100644 index b3540f3..0000000 --- a/FIREBASE_DONE.md +++ /dev/null @@ -1,190 +0,0 @@ -# ✅ Настройка Firebase завершена! - -## 📦 Что сделано: - -### 1. Gradle настроен правильно ✅ -- **Kotlin DSL** (`build.gradle.kts`) — современный подход -- **Google Services Plugin** версии 4.4.4 добавлен -- **Firebase BoM** версии 34.11.0 подключён -- Все Firebase зависимости добавлены: - - `firebase-analytics` - - `firebase-auth` - - `firebase-firestore` - - `firebase-messaging` - -### 2. Package name ✅ -- Application ID: `com.nanaxe.nanaxe_crm` -- Соответствует `google-services.json` - -### 3. AndroidManifest обновлён ✅ -- Добавлены разрешения для Firebase и уведомлений -- Название приложения: "NANAXE CRM" - -### 4. Правила безопасности Firestore ✅ -- Файл `firestore.rules` создан -- Доступ только для аутентифицированных пользователей -- Поддержка ролей (admin, member, viewer) - ---- - -## 🚀 СЛЕДУЮЩИЙ ШАГ: Загрузить правила безопасности - -### Открой Firebase Console: -1. Перейди в https://console.firebase.google.com -2. Выбери проект `nanax26-1f33b` -3. Слева: **Build** → **Firestore Database** -4. Вверху выбери вкладку **Rules** -5. Скопируй содержимое файла `firestore.rules` и вставь в редактор -6. Нажми **Publish** - -**Или используй Firebase CLI:** -```bash -npm install -g firebase-tools -firebase login -firebase use nanax26-1f33b -firebase deploy --only firestore:rules -``` - ---- - -## 📱 Запуск приложения - -### 1. Подключи устройство или запусти эмулятор -```bash -# Проверка подключенных устройств -flutter devices -``` - -### 2. Запусти приложение -```bash -flutter run -``` - -### 3. Если ошибка с SHA-1 ключом -Для Google Sign-In нужен SHA-1 fingerprint: - -```bash -# Для debug ключа (Windows) -cd android -gradlew app:signingReport -``` - -Скопируй SHA-1 из отчёта и добавь в Firebase Console: -1. Project Settings → Your apps → Android app -2. Add fingerprint -3. Вставь SHA-1 -4. Save - ---- - -## 🔧 Проверка работы - -### Чек-лист: -- [ ] Firebase проект создан -- [ ] Authentication включён (Google provider) -- [ ] Firestore база создана (region: eur3) -- [ ] Android приложение зарегистрировано -- [ ] `google-services.json` лежит в `android/app/` -- [ ] Gradle файлы обновлены -- [ ] Правила безопасности загружены в Firebase -- [ ] SHA-1 fingerprint добавлен (для Google Sign-In) - -### Тест аутентификации: -1. Запусти приложение -2. Нажми "Войти через Google" -3. Выбери аккаунт -4. Если успешно — переход на главный экран ✅ - ---- - -## 📊 Структура Firestore (автоматически создаётся) - -``` -users/ - └─ {userId}/ - ├─ email - ├─ displayName - ├─ role: "member" - ├─ teamIds: [] - └─ settings: {} - -teams/ - └─ {teamId}/ - ├─ name - ├─ memberIds: [] - └─ projects: subcollection - -projects/ - └─ {projectId}/ - ├─ name - ├─ ownerId - └─ tasks: subcollection - -tasks/ - └─ {taskId}/ - ├─ title - ├─ status: "todo" - ├─ priority: "medium" - └─ assigneeId - -mindmaps/ - └─ {projectId}/ - └─ nodes/ - └─ {nodeId}/ - ├─ title - ├─ level - └─ parentId - -events/ - └─ {eventId}/ - ├─ title - ├─ startTime - └─ attendeeIds - -alarms/ - └─ {alarmId}/ - ├─ title - ├─ dateTime - └─ isRepeating -``` - ---- - -## 🆘 Возможные ошибки - -### "No Firebase App" -``` -Проверь что google-services.json лежит в android/app/ -``` - -### "Sign-in failed" -``` -1. Добавь SHA-1 fingerprint в Firebase Console -2. Перескачай google-services.json -3. Пересобери приложение: flutter clean && flutter run -``` - -### "Permission denied" -``` -1. Проверь что правила безопасности загружены -2. Убедись что пользователь аутентифицирован -``` - -### "App not found" -``` -1. Проверь что package name совпадает: - - В google-services.json: "com.nanaxe.nanaxe_crm" - - В build.gradle.kts: applicationId = "com.nanaxe.nanaxe_crm" -``` - ---- - -## 📚 Документация - -- [Firebase Flutter Setup](https://firebase.flutter.dev/docs/overview) -- [Firestore Rules](https://firebase.google.com/docs/firestore/security/get-started) -- [Google Sign-In Flutter](https://pub.dev/packages/google_sign_in) - ---- - -**Готово! Запускай `flutter run`** 🚀 diff --git a/FIREBASE_SETUP.md b/FIREBASE_SETUP.md deleted file mode 100644 index 951147f..0000000 --- a/FIREBASE_SETUP.md +++ /dev/null @@ -1,24 +0,0 @@ -# Firebase configuration file -# Download this from Firebase Console: -# Project Settings > Your apps > Add app - -# For Android: -# 1. Add your app with package name: com.nanaxe.nanaxe_crm -# 2. Download google-services.json -# 3. Place it in: android/app/google-services.json - -# For iOS: -# 1. Add your app with bundle ID: com.nanaxe.nanaxeCrm -# 2. Download GoogleService-Info.plist -# 3. Place it in: ios/Runner/GoogleService-Info.plist - -# For Web: -# 1. Add web app -# 2. Copy firebaseConfig object -# 3. Update lib/core/firebase_options.dart with your config - -# Required Firebase APIs: -# - Authentication (Google Sign-In) -# - Firestore Database -# - Cloud Messaging (FCM) -# - Cloud Functions (optional, for AI processing) diff --git a/GITHUB_SETUP.md b/GITHUB_SETUP.md deleted file mode 100644 index 98eaaaa..0000000 --- a/GITHUB_SETUP.md +++ /dev/null @@ -1,349 +0,0 @@ -# 🚀 Настройка GitHub Actions и Автодеплоя - -## 📋 Содержание -1. [Создание репозитория](#создание-репозитория) -2. [Настройка Secrets](#настройка-secrets) -3. [Настройка VPS](#настройка-vps) -4. [Первый деплой](#первый-деплой) -5. [Автоматический деплой](#автоматический-деплой) - ---- - -## Создание репозитория - -### 1. Инициализация Git - -```bash -# В корне проекта -cd c:\Users\ormix\StudioProjects\NANAXE -git init - -# Добавьте .gitignore -echo "# Flutter/Dart -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -build/ -*.g.dart -*.freezed.dart - -# IDE -.idea/ -.vscode/ -*.iml -*.ipr -*.iws - -# Environment -.env -.env.local - -# Server -server/node_modules/ -server/logs/ -server/backups/ -*.log - -# OS -.DS_Store -Thumbs.db -" > .gitignore - -# Первый коммит -git add . -git commit -m "Initial commit: Nanaxe CRM with server" -``` - -### 2. Создание репозитория на GitHub - -```bash -# Создайте репозиторий на GitHub (через веб-интерфейс) -# Затем: - -git remote add origin https://github.com/YOUR_USERNAME/nanaxe-crm.git -git branch -M main -git push -u origin main -``` - ---- - -## Настройка Secrets - -Перейдите в **Settings → Secrets and variables → Actions** и добавьте: - -### Обязательные Secrets - -| Secret Name | Description | Example | -|-------------|-------------|---------| -| `VPS_HOST` | IP или домен VPS | `203.0.113.1` или `nanaxe.example.com` | -| `VPS_USER` | SSH пользователь | `root` или `deploy` | -| `VPS_SSH_KEY` | Приватный SSH ключ | Содержимое `~/.ssh/id_rsa` | -| `DEPLOY_PATH` | Путь на сервере | `/opt/nanaxe-server` | - -### Опциональные Secrets - -| Secret Name | Description | -|-------------|-------------| -| `GITEA_DOMAIN` | Домен для Gitea | -| `GITEA_TOKEN` | API токен Gitea | -| `JWT_SECRET` | Секретный ключ JWT | -| `POSTGRES_PASSWORD` | Пароль PostgreSQL | -| `WEBDAV_USER` | WebDAV пользователь | -| `WEBDAV_PASSWORD` | WebDAV пароль | - -### Генерация SSH ключа - -На вашем локальном компьютере: - -```bash -# Сгенерируйте ключ (без пароля для CI/CD) -ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/nanaxe_deploy - -# Добавьте публичный ключ на VPS -ssh-copy-id -i ~/.ssh/nanaxe_deploy.pub root@YOUR_VPS_IP - -# Скопируйте приватный ключ для GitHub -cat ~/.ssh/nanaxe_deploy -# Скопируйте вывод в секрет VPS_SSH_KEY -``` - ---- - -## Настройка VPS - -### 1. Быстрая настройка (автоматическая) - -```bash -# На VPS выполните: -curl -fsSL https://raw.githubusercontent.com/YOUR_USERNAME/nanaxe-crm/main/server/scripts/setup-vps.sh | bash -``` - -### 2. Ручная настройка - -```bash -# Скопируйте скрипт на VPS -scp server/scripts/setup-vps.sh root@YOUR_VPS_IP:/tmp/ - -# Выполните -ssh root@YOUR_VPS_IP -chmod +x /tmp/setup-vps.sh -/tmp/setup-vps.sh -``` - -### 3. Что делает скрипт - -- ✅ Обновляет систему -- ✅ Устанавливает Docker и Docker Compose -- ✅ Настраивает UFW firewall -- ✅ Устанавливает Fail2Ban -- ✅ Создаёт директорию деплоя -- ✅ Настраивает SSL (Let's Encrypt) -- ✅ Создаёт systemd сервис -- ✅ Генерирует секреты - ---- - -## Первый деплой - -### 1. Локально настройте окружение - -```bash -cd server -cp .env.example .env -# Отредактируйте .env -``` - -### 2. Запушьте на GitHub - -```bash -git add . -git commit -m "Add server deployment configuration" -git push origin main -``` - -### 3. Запустите GitHub Actions - -Перейдите в **Actions** вкладку на GitHub и запустите workflow вручную или дождитесь автоматического запуска. - -### 4. Проверьте деплой - -```bash -# На VPS -ssh root@YOUR_VPS_IP -cd /opt/nanaxe-server/server -docker compose -f docker-compose.prod.yml ps -docker compose -f docker-compose.prod.yml logs -f -``` - ---- - -## Автоматический деплой - -### Как это работает - -``` -┌─────────────┐ ┌─────────────┐ ┌─────────────┐ -│ Push to │────→│ GitHub │────→│ Build & │ -│ main │ │ Actions │ │ Push │ -└─────────────┘ └─────────────┘ │ Docker │ - │ Image │ - └──────┬──────┘ - │ - ▼ - ┌─────────────┐ - │ Deploy │ - │ to VPS │ - └──────┬──────┘ - │ - ▼ - ┌─────────────┐ - │ Health │ - │ Check │ - └─────────────┘ -``` - -### Триггеры деплоя - -Автодеплой запускается при: -- Push в `main` или `master` ветку -- Merge Pull Request -- Ручной запуск через GitHub UI - -### Проверка статуса - -```bash -# На VPS -docker compose -f docker-compose.prod.yml ps - -# Логи API -docker compose -f docker-compose.prod.yml logs api -f - -# Логи Gitea -docker compose -f docker-compose.prod.yml logs gitea -f -``` - ---- - -## 🔧 Полезные команды - -### Ручной деплой - -```bash -# На VPS -cd /opt/nanaxe-server/server -bash scripts/deploy.sh -``` - -### Откат (Rollback) - -```bash -# Через GitHub Actions -# Actions → Deploy Nanaxe Server → Run workflow → Use workflow from: main -# Выберите "Rollback" (если добавлено) или вручную: - -# На VPS -docker compose -f docker-compose.prod.yml down -docker compose -f docker-compose.prod.yml up -d -``` - -### Обновление - -```bash -# Локально - git pull origin main -# Внесите изменения -git add . && git commit -m "Update" && git push - -# GitHub Actions автоматически задеплоит -``` - -### Мониторинг - -```bash -# Статус сервисов -docker compose -f docker-compose.prod.yml ps - -# Использование ресурсов -docker stats - -# Логи в реальном времени -docker compose -f docker-compose.prod.yml logs -f --tail=100 -``` - ---- - -## 🐛 Устранение неполадок - -### Проблема: Деплой не запускается - -```bash -# Проверьте GitHub Actions logs -# Actions → Deploy Nanaxe Server → Последний run -``` - -### Проблема: SSH подключение не работает - -```bash -# На VPS проверьте ключ - cat ~/.ssh/authorized_keys - -# Проверьте права - chmod 700 ~/.ssh - chmod 600 ~/.ssh/authorized_keys -``` - -### Проблема: Контейнеры не стартуют - -```bash -# На VPS -cd /opt/nanaxe-server/server -docker compose -f docker-compose.prod.yml down -docker compose -f docker-compose.prod.yml up -d -docker compose -f docker-compose.prod.yml logs -``` - -### Проблема: Health check failed - -```bash -# Проверьте API -curl http://localhost:8080/health - -# Проверьте Gitea -curl http://localhost:3000/api/healthz -``` - ---- - -## 📊 Workflow файлы - -### Основной деплой: `.github/workflows/deploy.yml` - -- Собирает Docker образ -- Публикует в GitHub Container Registry -- Деплоит на VPS -- Запускает health checks -- Поддерживает rollback - -### PR Checks: `.github/workflows/pr-check.yml` - -- Запускается на Pull Request -- Проверяет код -- Тестирует сборку Docker -- Сканирует уязвимости -- Валидирует docker-compose - ---- - -## 🎉 Готово! - -Теперь при каждом push в `main`: -1. ✅ Код автоматически тестируется -2. ✅ Docker образ собирается -3. ✅ Образ публикуется в GitHub Container Registry -4. ✅ VPS автоматически обновляется -5. ✅ Health checks проверяют работоспособность - -Ваш сервер всегда будет актуальным! 🚀 diff --git a/MOCK_VS_REAL_API.md b/MOCK_VS_REAL_API.md deleted file mode 100644 index 7694844..0000000 --- a/MOCK_VS_REAL_API.md +++ /dev/null @@ -1,162 +0,0 @@ -# 🔄 Как включить реальный API DeepSeek - -## Текущий режим: MOCK ✅ - -Сейчас приложение работает с **mock данными** (без реальных запросов к API). - -Это означает: -- ✅ Все функции работают без интернета -- ✅ Не расходуется баланс DeepSeek -- ⚠️ Ответы не от AI, а заготовленные шаблоны - ---- - -## Когда включить реальный API - -### Шаг 1: Проверьте баланс DeepSeek - -1. Зайдите на https://platform.deepseek.com/billing -2. Убедитесь что на счету есть средства (минимум $1-2) -3. Если баланс пуст - пополните счёт - -### Шаг 2: Измените режим в коде - -Откройте файл: `lib/core/deepseek_service.dart` - -Найдите строку (примерно строка 12): -```dart -final bool useMockMode = true; -``` - -Измените на: -```dart -final bool useMockMode = false; -``` - -### Шаг 3: Перезапустите приложение - -```bash -flutter run -``` - ---- - -## Как это работает - -### Mock режим (useMockMode = true) -``` -Запрос → DeepSeek Service → Mock данные → Ответ - (без API запросов) -``` - -**Плюсы:** -- Быстро (без сетевых задержек) -- Бесплатно (не нужен API) -- Стабильно (всегда работает) - -**Минусы:** -- Ответы не от AI -- Не адаптированы под контекст - -### Реальный API (useMockMode = false) -``` -Запрос → DeepSeek Service → DeepSeek API → AI ответ - (реальный запрос) -``` - -**Плюсы:** -- Умные ответы от AI -- Адаптация под контекст -- Качественные описания - -**Минусы:** -- Расходует токены (~$0.001-0.01 за запрос) -- Нужен интернет -- Медленнее (1-3 секунды) - ---- - -## Проверка работы API - -После включения реального API, запустите тест: - -```bash -dart run test_deepseek_api.dart -``` - -Если видите ✅ вместо ❌ - API работает! - ---- - -##Fallback режим - -Даже при `useMockMode = false`, если API запрос не прошёл (нет баланса, ошибка сети), -сервис автоматически вернётся к mock данным. - -В консоли увидите: -``` -DeepSeek API Error: 402 - Insufficient Balance -⚠️ Используется mock режим как fallback -``` - ---- - -## Стоимость запросов - -Примерные расходы на DeepSeek API: - -| Действие | Токенов | Стоимость | -|----------|---------|-----------| -| Генерация подзадач | ~500 | $0.0001 | -| Приоритизация | ~100 | $0.00003 | -| Описание задачи | ~200 | $0.00006 | -| Планирование | ~1000 | $0.0003 | -| Mind-карта | ~1500 | $0.0004 | - -**При $2 на счету** можно сделать примерно: -- 20,000 генераций подзадач -- 60,000 приоритизаций -- 5,000 mind-карт - ---- - -## Рекомендации - -### Для разработки и тестирования -Оставьте **mock режим** (`useMockMode = true`) -- Быстрее цикл разработки -- Не тратите токены - -### Для продакшена -Включите **реальный API** (`useMockMode = false`) -- Пользователи получат AI функции -- Расходы минимальные - ---- - -## Troubleshooting - -### Ошибка 402: Insufficient Balance -``` -1. Проверьте баланс: https://platform.deepseek.com/billing -2. Пополните счёт -3. Или верните useMockMode = true -``` - -### Ошибка 401: Invalid API Key -``` -1. Проверьте что ключ в .env начинается с 'sk-' -2. Нет лишних пробелов -3. Ключ активен в Dashboard -``` - -### Ошибка сети -``` -1. Проверьте интернет-соединение -2. DeepSeek может быть недоступен в вашем регионе -3. Сервис автоматически переключится на mock -``` - ---- - -**Готово! 🚀** diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md deleted file mode 100644 index fbb4b71..0000000 --- a/PROJECT_STRUCTURE.md +++ /dev/null @@ -1,208 +0,0 @@ -# 📁 Структура проекта NANAXE CRM - -``` -NANAXE/ -│ -├── 📄 README.md # Общая информация о проекте -├── 📄 QUICKSTART.md # Быстрый старт и настройка -├── 📄 ARCHITECTURE.md # Архитектурное описание -├── 📄 FIREBASE_SETUP.md # Инструкция по настройке Firebase -├── 📄 .env.example # Шаблон переменных окружения -├── 📄 .gitignore # Git ignore файл -├── 📄 pubspec.yaml # Зависимости Flutter -│ -├── 📂 lib/ # Исходный код приложения -│ │ -│ ├── 📄 main.dart # Точка входа -│ │ -│ ├── 📂 core/ # Ядро приложения -│ │ ├── firebase_config.dart # Конфигурация Firebase -│ │ ├── firestore_service.dart # Сервис для работы с Firestore -│ │ ├── local_storage_service.dart # Локальное хранилище (Hive) -│ │ ├── ai_service.dart # AI сервис (OpenAI, Anthropic) -│ │ ├── router.dart # Маршрутизация (GoRouter) -│ │ └── theme.dart # Темы оформления -│ │ -│ ├── 📂 models/ # Модели данных -│ │ ├── user_model.dart # Пользователь -│ │ ├── task_model.dart # Задача -│ │ ├── project_model.dart # Проект -│ │ ├── mind_map_model.dart # Mind-карта -│ │ ├── calendar_event_model.dart # Событие календаря -│ │ └── alarm_model.dart # Будильник/Напоминание -│ │ -│ ├── 📂 providers/ # State Management (Riverpod) -│ │ └── auth_provider.dart # Провайдер аутентификации -│ │ -│ ├── 📂 screens/ # Экраны приложения -│ │ ├── auth_screen.dart # Экран входа -│ │ ├── home_screen.dart # Главная навигация -│ │ ├── dashboard_screen.dart # Дашборд -│ │ ├── tasks_screen.dart # Задачи -│ │ ├── mind_map_screen.dart # Mind-карта -│ │ ├── calendar_screen.dart # Календарь -│ │ └── alarms_screen.dart # Будильники -│ │ -│ ├── 📂 widgets/ # Переиспользуемые виджеты -│ │ └── (создаётся по мере необходимости) -│ │ -│ └── 📂 features/ # Функциональные модули -│ └── (для будущего расширения) -│ -├── 📂 assets/ # Ресурсы приложения -│ ├── 📂 images/ # Изображения -│ │ └── .gitkeep -│ └── 📂 icons/ # Иконки -│ └── .gitkeep -│ -├── 📂 test/ # Тесты -│ └── widget_test.dart # Widget тесты -│ -├── 📂 android/ # Android платформа -│ ├── 📂 app/ -│ │ ├── src/main/ -│ │ │ ├── AndroidManifest.xml -│ │ │ ├── kotlin/ # Kotlin код -│ │ │ ├── res/ # Ресурсы Android -│ │ │ └── ... -│ │ └── build.gradle # Android build config -│ └── google-services.json # ⚠️ Firebase config (нужно добавить) -│ -├── 📂 ios/ # iOS платформа -│ ├── 📂 Runner/ -│ │ ├── AppDelegate.swift -│ │ ├── Info.plist -│ │ └── ... -│ └── GoogleService-Info.plist # ⚠️ Firebase config (нужно добавить) -│ -├── 📂 web/ # Web платформа -│ ├── index.html -│ └── manifest.json -│ -├── 📂 windows/ # Windows платформа -│ └── ... -│ -├── 📂 linux/ # Linux платформа -│ └── ... -│ -├── 📂 macos/ # macOS платформа -│ └── ... -│ -└── 📂 build/ # ⚠️ Build artifacts (не в git) - └── (генерируется при сборке) -``` - -## 🔑 Ключевые файлы - -### Конфигурация -| Файл | Описание | -|------|----------| -| `pubspec.yaml` | Зависимости и настройки Flutter | -| `.gitignore` | Исключения для Git | -| `.env.example` | Шаблон переменных окружения | - -### Firebase (нужно добавить) -| Файл | Где взять | Куда положить | -|------|-----------|---------------| -| `google-services.json` | Firebase Console → Android | `android/app/` | -| `GoogleService-Info.plist` | Firebase Console → iOS | `ios/Runner/` | - -### Основные модули -| Модуль | Файл | Описание | -|--------|------|----------| -| **Entry Point** | `lib/main.dart` | Инициализация и запуск | -| **Firebase** | `lib/core/firebase_config.dart` | Настройка Firebase | -| **Auth** | `lib/providers/auth_provider.dart` | Аутентификация через Google | -| **Firestore** | `lib/core/firestore_service.dart` | CRUD операции с БД | -| **AI** | `lib/core/ai_service.dart` | Интеграция с OpenAI | -| **Storage** | `lib/core/local_storage_service.dart` | Офлайн кэш на Hive | -| **Router** | `lib/core/router.dart` | Навигация между экранами | -| **Theme** | `lib/core/theme.dart` | Светлая/тёмная темы | - -## 📦 Основные зависимости - -### State Management -- `flutter_riverpod: ^2.5.0` - Реактивное управление состоянием -- `riverpod_annotation: ^2.3.5` - Code generation для Riverpod - -### Firebase -- `firebase_core: ^2.32.0` - Ядро Firebase -- `firebase_auth: ^4.19.0` - Аутентификация -- `cloud_firestore: ^4.17.0` - Firestore база данных -- `firebase_messaging: ^14.9.0` - Push уведомления - -### Local Storage -- `hive_flutter: ^1.1.0` - Быстрое локальное хранилище -- `hive: ^2.2.3` - Hive ядро - -### Navigation -- `go_router: ^13.1.0` - Декларативная маршрутизация - -### AI -- `http: ^1.2.0` - HTTP клиент для API запросов -- `dio: ^5.4.0` - Продвинутый HTTP клиент - -### UI -- `graphview: ^1.2.0` - Визуализация графов (для mind-карт) -- `flutter_mind_map: ^1.0.23` - Готовый mind-map виджет -- `flutter_staggered_grid_view: ^0.7.0` - Сетка для карточек - -### Google Integration -- `google_sign_in: ^6.2.1` - Вход через Google -- `googleapis: ^13.2.0` - Google API (Calendar и др.) - -### Notifications -- `flutter_local_notifications: ^17.0.0` - Локальные уведомления -- `workmanager: ^0.5.2` - Фоновые задачи - -## 🎯 Точки расширения - -### Для добавления новой фичи: - -1. **Создать модель** в `lib/models/` -2. **Создать сервис** в `lib/core/` или `lib/features/` -3. **Создать провайдер** в `lib/providers/` -4. **Создать экран** в `lib/screens/` -5. **Добавить маршрут** в `lib/core/router.dart` -6. **Добавить виджеты** в `lib/widgets/` - -### Пример: Добавление заметок - -``` -lib/ -├── models/ -│ └── note_model.dart # ✨ Новая модель -├── features/ -│ └── notes/ -│ ├── note_service.dart # ✨ Сервис -│ └── note_provider.dart # ✨ Провайдер -├── screens/ -│ └── notes_screen.dart # ✨ Экран -└── widgets/ - └── note_card.dart # ✨ Виджет -``` - -## 📊 Метрики проекта - -``` -Строки кода: ~2000+ -Файлов: ~20 -Модулей: 6 (Auth, Tasks, MindMap, Calendar, Alarms, Dashboard) -Моделей данных: 6 -Экранов: 6 -Сервисов: 4 (Firebase, Firestore, AI, LocalStorage) -``` - -## 🔄 CI/CD (будущее) - -```yaml -.github/workflows/ -├── build.yml # Сборка APK -├── test.yml # Запуск тестов -└── deploy.yml # Деплой в Store -``` - ---- - -**Структура создана для команды NANAXE** 🚀 -Последнее обновление: Март 2026 diff --git a/QUICKSTART.md b/QUICKSTART.md deleted file mode 100644 index 9918f99..0000000 --- a/QUICKSTART.md +++ /dev/null @@ -1,340 +0,0 @@ -# 🚀 Быстрый старт - NANAXE CRM - -## ✅ Что уже создано - -### Структура проекта -``` -lib/ -├── core/ # Ядро приложения -│ ├── ai_service.dart # AI сервис (OpenAI, Anthropic) -│ ├── firebase_config.dart # Firebase конфигурация -│ ├── firestore_service.dart # Firestore сервис (CRUD операции) -│ ├── local_storage_service.dart # Локальное хранилище (Hive) -│ ├── router.dart # Маршрутизация (GoRouter) -│ └── theme.dart # Темы оформления -├── features/ # Функциональные модули (будущие) -├── models/ # Модели данных -│ ├── user_model.dart # Пользователь -│ ├── task_model.dart # Задача -│ ├── project_model.dart # Проект -│ ├── mind_map_model.dart # Mind-карта -│ ├── calendar_event_model.dart # Событие календаря -│ └── alarm_model.dart # Будильник -├── providers/ # State management (Riverpod) -│ └── auth_provider.dart # Аутентификация -├── screens/ # Экраны -│ ├── auth_screen.dart # Вход -│ ├── home_screen.dart # Главная навигация -│ ├── dashboard_screen.dart # Дашборд -│ ├── tasks_screen.dart # Задачи -│ ├── mind_map_screen.dart # Mind-карта -│ ├── calendar_screen.dart # Календарь -│ └── alarms_screen.dart # Будильники -└── widgets/ # Виджеты (будущие) -``` - -## 🔧 Настройка Firebase - -### 1. Создайте проект в Firebase Console - -1. Перейдите на https://console.firebase.google.com -2. Нажмите "Add project" -3. Введите имя: "NANAXE CRM" -4. Отключите Google Analytics (не нужно для начала) -5. Создайте проект - -### 2. Включите Authentication - -1. В меню слева: **Build** → **Authentication** -2. Нажмите **Get started** -3. Включите **Google** провайдер: - - Click **Google** → **Enable** - - Enter support email: ваш email - - **Save** - -### 3. Создайте Firestore базу - -1. В меню: **Build** → **Firestore Database** -2. Нажмите **Create database** -3. Выберите **Start in test mode** (потом настроим правила) -4. Выберите локацию (например, us-central) -5. **Enable** - -### 4. Добавьте Android приложение - -1. В Firebase Console: **Project Settings** (шестерёнка) -2. **Your apps** → **Add app** → **Android** -3. Введите package name: `com.nanahui.nanahui_crm` -4. Скачайте `google-services.json` -5. Положите файл в: `android/app/google-services.json` - -### 5. Добавьте iOS приложение (если нужно) - -1. **Add app** → **iOS** -2. Bundle ID: `com.nanahui.nanahuiCrm` -3. Скачайте `GoogleService-Info.plist` -4. Положите в: `ios/Runner/GoogleService-Info.plist` - -### 6. Настройте android/app/build.gradle - -В файле `android/app/build.gradle` добавьте: - -```gradle -dependencies { - // ... другие зависимости - - // Firebase - implementation platform('com.google.firebase:firebase-bom:32.7.0') - implementation 'com.google.firebase:firebase-analytics' - implementation 'com.google.firebase:firebase-auth' - implementation 'com.google.firebase:firebase-firestore' - implementation 'com.google.firebase:firebase-messaging' -} - -// В конце файла: -apply plugin: 'com.google.gms.google-services' -``` - -## 🔑 Настройка API ключей - -### OpenAI API (для AI функций) - -1. Перейдите на https://platform.openai.com/api-keys -2. Создайте новый API ключ -3. Скопируйте ключ - -### Google Calendar API - -1. Перейдите на https://console.cloud.google.com -2. Создайте новый проект или выберите существующий -3. Включите **Google Calendar API** -4. Создайте OAuth 2.0 credentials -5. Получите Client ID и Client Secret - -## 📝 Настройка окружения - -1. Скопируйте `.env.example` в `.env`: -```bash -cp .env.example .env -``` - -2. Заполните `.env` своими ключами: -``` -OPENAI_API_KEY=sk-your-actual-openai-key -FIREBASE_API_KEY=your-firebase-key -# и т.д. -``` - -## 🏃 Запуск приложения - -### 1. Установите зависимости -```bash -flutter pub get -``` - -### 2. Запуск на эмуляторе/устройстве -```bash -flutter run -``` - -### 3. Сборка APK (Android) -```bash -flutter build apk --release -``` - -### 4. Сборка для iOS -```bash -flutter build ios --release -``` - -## 📱 Тестирование - -### Проверка аутентификации -1. Запустите приложение -2. Нажмите "Войти через Google" -3. Выберите аккаунт -4. Должна произойти авторизация и переход на главный экран - -### Проверка навигации -1. После входа используйте нижнюю навигационную панель -2. Переключайтесь между: Главная, Задачи, Mind-карта, Календарь, Будильники - -## 🔥 Firestore правила безопасности - -Создайте в Firebase Console → Firestore Database → Rules: - -```javascript -rules_version = '2'; -service cloud.firestore { - match /databases/{database}/documents { - // Helper function - function isAuthenticated() { - return request.auth != null; - } - - function isOwner(userId) { - return request.auth.uid == userId; - } - - // Users - match /users/{userId} { - allow read: if isAuthenticated(); - allow write: if isOwner(userId); - } - - // Projects - match /projects/{projectId} { - allow read: if isAuthenticated(); - allow create: if isAuthenticated(); - allow update, delete: if isAuthenticated(); - } - - // Tasks - match /tasks/{taskId} { - allow read: if isAuthenticated(); - allow create, update, delete: if isAuthenticated(); - } - - // Events - match /events/{eventId} { - allow read: if isAuthenticated(); - allow create, update, delete: if isAuthenticated(); - } - - // Alarms - match /alarms/{alarmId} { - allow read: if isAuthenticated(); - allow create, update, delete: if isAuthenticated(); - } - - // Mind maps - match /mindmaps/{projectId}/nodes/{nodeId} { - allow read: if isAuthenticated(); - allow write: if isAuthenticated(); - } - } -} -``` - -## 📊 Структура Firestore - -``` -users/ - └─ {userId}/ - ├─ email: string - ├─ displayName: string - ├─ photoUrl: string - ├─ role: "admin" | "member" | "viewer" - ├─ teamIds: string[] - └─ settings: map - -projects/ - └─ {projectId}/ - ├─ name: string - ├─ description: string - ├─ teamId: string - ├─ ownerId: string - ├─ memberIds: string[] - ├─ status: string - ├─ startDate: timestamp - ├─ endDate: timestamp - └─ color: string - -tasks/ - └─ {taskId}/ - ├─ title: string - ├─ description: string - ├─ projectId: string - ├─ parentTaskId: string? - ├─ status: "todo" | "inProgress" | "review" | "done" - ├─ priority: "low" | "medium" | "high" | "urgent" - ├─ assigneeId: string? - ├─ creatorId: string - ├─ dueDate: timestamp? - └─ aiMetadata: map - -mindmaps/ - └─ {projectId}/ - └─ nodes/ - └─ {nodeId}/ - ├─ parentId: string? - ├─ level: number - ├─ title: string - ├─ taskId: string? - ├─ x: number - ├─ y: number - └─ color: string - -events/ - └─ {eventId}/ - ├─ title: string - ├─ startTime: timestamp - ├─ endTime: timestamp - ├─ googleEventId: string? - └─ attendeeIds: string[] - -alarms/ - └─ {alarmId}/ - ├─ title: string - ├─ dateTime: timestamp - ├─ isRepeating: boolean - └─ taskId: string? -``` - -## 🛠 Следующие шаги - -### Первоочередные задачи: - -1. ✅ ~~Создать базовую структуру~~ -2. ✅ ~~Настроить модели данных~~ -3. ✅ ~~Создать экраны навигации~~ -4. 🔲 Настроить Firebase проект -5. 🔲 Реализовать реальную аутентификацию -6. 🔲 Добавить CRUD операции с Firestore -7. 🔲 Реализовать синхронизацию офлайн-данных -8. 🔲 Добавить AI функции (OpenAI API) -9. 🔲 Интеграция с Google Calendar -10. 🔲 Создать mind-map виджет с drag-drop - -### Для тестирования AI: - -1. Добавьте OpenAI API ключ в `lib/core/ai_service.dart`: -```dart -final aiService = AIService( - openaiApiKey: 'sk-your-api-key-here', -); -``` - -2. Протестируйте генерацию подзадач: -```dart -final subtasks = await aiService.generateSubtasks( - title: 'Создать CRM систему', - description: 'Разработка CRM для управления задачами команды', -); -print(subtasks); -``` - -## 🆘 Troubleshooting - -### Ошибка: "No Firebase App" -- Убедитесь, что `google-services.json` лежит в `android/app/` -- Проверьте, что добавлен `apply plugin: 'com.google.gms.google-services'` - -### Ошибка: "Sign-in failed" -- Проверьте SHA-1 fingerprint для Android -- Убедитесь, что Google Sign-In включён в Firebase Console - -### Ошибка: "Permission denied" -- Проверьте правила безопасности Firestore -- Убедитесь, что пользователь аутентифицирован - -## 📚 Ресурсы - -- [Flutter документация](https://docs.flutter.dev) -- [Riverpod документация](https://riverpod.dev) -- [Firebase Flutter](https://firebase.flutter.dev) -- [OpenAI API](https://platform.openai.com/docs) - ---- - -**Создано для команды NANAXE** 🚀 diff --git a/SETUP_ADMINS.md b/SETUP_ADMINS.md deleted file mode 100644 index 0a0136a..0000000 --- a/SETUP_ADMINS.md +++ /dev/null @@ -1,65 +0,0 @@ -# 📝 Настройка админов в Firestore - -## Админы NANAXE CRM - -### Email админов: -1. `almalexyz@gmail.com` -2. `ormix16@gmail.com` - -## Как назначить админа: - -### Способ 1: Через Firebase Console - -1. Открой https://console.firebase.google.com/project/nanax26-1f33b/firestore -2. Найди коллекцию `users` -3. Найди документ пользователя по email -4. Измени поле `role` на `"admin"` - -### Способ 2: Через Firestore запрос - -```javascript -// В Firebase Console → Firestore → Start collection -db.collection('users').where('email', '==', 'almalexyz@gmail.com').get() - .then(snapshot => { - snapshot.docs.forEach(doc => { - doc.ref.update({ role: 'admin' }); - }); - }); - -db.collection('users').where('email', '==', 'ormix16@gmail.com').get() - .then(snapshot => { - snapshot.docs.forEach(doc => { - doc.ref.update({ role: 'admin' }); - }); - }); -``` - -### Способ 3: Через Cloud Functions (автоматически) - -```javascript -exports.setOnAdminLogin = functions.auth.user().onCreate(async (user) => { - const adminEmails = ['almalexyz@gmail.com', 'ormix16@gmail.com']; - - if (adminEmails.includes(user.email)) { - await admin.firestore().collection('users').doc(user.uid).update({ - role: 'admin' - }); - } -}); -``` - -## Проверка роли: - -В коде приложения: -```dart -final user = ref.watch(currentUserProvider); -if (user?.isAdmin ?? false) { - // Показать админку -} -``` - -## Роли: - -- `admin` - полный доступ ко всем функциям -- `member` - создание задач, редактирование своих -- `viewer` - только просмотр diff --git a/SETUP_COMPLETE.md b/SETUP_COMPLETE.md deleted file mode 100644 index c7eab43..0000000 --- a/SETUP_COMPLETE.md +++ /dev/null @@ -1,224 +0,0 @@ -# ✅ НАСТРОЙКА ЗАВЕРШЕНА! - -## 📊 Что сделано: - -### Firebase -- ✅ Проект создан: `nanax26-1f33b` -- ✅ Firestore база в регионе `eur3` -- ✅ Authentication включён (Google Provider) -- ✅ Android приложение зарегистрировано -- ✅ `google-services.json` добавлен -- ✅ Firebase BoM 34.11.0 подключён -- ✅ Все Firebase зависимости добавлены - -### Android -- ✅ Kotlin DSL (`build.gradle.kts`) -- ✅ Package: `com.nanaxe.nanaxe_crm` -- ✅ Google Services Plugin 4.4.4 -- ✅ SHA-1 получен: `29:A6:36:C1:74:29:43:CE:2E:59:B8:28:46:FF:56:40:2B:9B:92:45` - -### Flutter -- ✅ Зависимости установлены -- ✅ Код готов (3 info warning только о print()) -- ✅ Структура проекта создана -- ✅ 6 экранов готовы -- ✅ Модели данных созданы -- ✅ Сервисы (Firebase, AI, Storage) готовы - ---- - -## 🔑 СЛЕДУЮЩИЕ ШАГИ: - -### 1. Добавь SHA-1 в Firebase Console - -**Сейчас же сделай это:** - -1. Открой: https://console.firebase.google.com/project/nanax26-1f33b/settings/general/android:com.nanaxe.nanaxe_crm -2. Нажми **Add fingerprint** в разделе "SHA certificate fingerprints" -3. Вставь: `29:A6:36:C1:74:29:43:CE:2E:59:B8:28:46:FF:56:40:2B:9B:92:45` -4. Сохрани -5. Перескачай `google-services.json` -6. Замени файл в `c:\Users\ormix\NANAHUI\google-services.json` - -### 2. Загрузи правила безопасности Firestore - -1. Открой: https://console.firebase.google.com/project/nanax26-1f33b/firestore/rules -2. Открой файл `c:\Users\ormix\NANAHUI\firestore.rules` -3. Скопируй всё содержимое -4. Вставь в редактор на сайте (замени текущие правила `allow read, write: if false;`) -5. Нажми **Publish** - -### 3. Запусти приложение - -```bash -cd c:\Users\ormix\NANAXE -flutter run -``` - ---- - -## 📱 Проверка работы - -### Тест 1: Аутентификация -1. Запусти приложение -2. Нажми "Войти через Google" -3. Выбери аккаунт -4. ✅ Должна произойти авторизация и переход на главный экран - -### Тест 2: Навигация -1. После входа используй нижнюю панель -2. Переключайся между: Главная, Задачи, Mind-карта, Календарь, Будильники -3. ✅ Все экраны должны открываться - -### Тест 3: Firestore (после загрузки правил) -1. Создай задачу через консоль отладки -2. Проверь что она появилась в Firestore Console -3. ✅ Данные должны сохраняться - ---- - -## 📁 Структура проекта - -``` -NANAHUI/ -├── 📄 README.md # Общая информация -├── 📄 QUICKSTART.md # Быстрый старт -├── 📄 ARCHITECTURE.md # Архитектура -├── 📄 FIREBASE_SETUP.md # Настройка Firebase -├── 📄 FIREBASE_DONE.md # Итоги настройки -├── 📄 ADD_SHA1.md # Как добавить SHA-1 -├── 📄 PROJECT_STRUCTURE.md # Структура проекта -├── 📄 SETUP_COMPLETE.md # Этот файл -│ -├── 📂 lib/ -│ ├── 📄 main.dart # Точка входа -│ ├── 📂 core/ # Сервисы -│ │ ├── firebase_config.dart -│ │ ├── firestore_service.dart -│ │ ├── local_storage_service.dart -│ │ ├── ai_service.dart -│ │ ├── router.dart -│ │ └── theme.dart -│ ├── 📂 models/ # Модели данных -│ │ ├── user_model.dart -│ │ ├── task_model.dart -│ │ ├── project_model.dart -│ │ ├── mind_map_model.dart -│ │ ├── calendar_event_model.dart -│ │ └── alarm_model.dart -│ ├── 📂 providers/ # State management -│ │ └── auth_provider.dart -│ └── 📂 screens/ # Экраны -│ ├── auth_screen.dart -│ ├── home_screen.dart -│ ├── dashboard_screen.dart -│ ├── tasks_screen.dart -│ ├── mind_map_screen.dart -│ ├── calendar_screen.dart -│ └── alarms_screen.dart -│ -├── 📂 android/ -│ ├── 📂 app/ -│ │ ├── build.gradle.kts # ✅ Настроен -│ │ └── src/main/AndroidManifest.xml # ✅ Разрешения -│ └── google-services.json # ✅ Firebase config -│ -└── firestore.rules # ✅ Правила безопасности -``` - ---- - -## 🔧 Конфигурация - -### Firebase Project -- **Project ID**: `nanax26-1f33b` -- **Package**: `com.nanaxe.nanaxe_crm` -- **Region**: `eur3` (Europe) - -### SHA-1 Fingerprint -``` -29:A6:36:C1:74:29:43:CE:2E:59:B8:28:46:FF:56:40:2B:9B:92:45 -``` - -### Firebase Dependencies -```kotlin -implementation(platform("com.google.firebase:firebase-bom:34.11.0")) -implementation("com.google.firebase:firebase-analytics") -implementation("com.google.firebase:firebase-auth") -implementation("com.google.firebase:firebase-firestore") -implementation("com.google.firebase:firebase-messaging") -``` - ---- - -## 🆘 Если что-то не работает - -### Ошибка: "Sign-in failed" -``` -1. Проверь что SHA-1 добавлен в Firebase Console -2. Перескачай google-services.json -3. flutter clean && flutter run -``` - -### Ошибка: "Permission denied" -``` -1. Загрузи правила безопасности из firestore.rules -2. Проверь что пользователь аутентифицирован -``` - -### Ошибка: "No Firebase App" -``` -1. Проверь что google-services.json лежит в android/app/ -2. Проверь что package name совпадает -``` - -### Ошибка: "App not found" -``` -1. Проверь что package name в google-services.json совпадает с build.gradle.kts -2. Должно быть: com.nanaxe.nanaxe_crm -``` - ---- - -## 📚 Документация - -- [Firebase Flutter](https://firebase.flutter.dev) -- [Firestore Rules](https://firebase.google.com/docs/firestore/security/get-started) -- [Google Sign-In](https://pub.dev/packages/google_sign_in) -- [Flutter Docs](https://docs.flutter.dev) - ---- - -## 🎯 Что дальше? - -### Ближайшие задачи: -1. ✅ Настроить Firebase (ГОТОВО) -2. ✅ Получить SHA-1 (ГОТОВО) -3. ⏳ Добавить SHA-1 в Firebase Console -4. ⏳ Загрузить правила безопасности -5. ⏳ Протестировать аутентификацию -6. ⏳ Реализовать CRUD задач -7. ⏳ Добавить AI функции -8. ⏳ Интеграция с Google Calendar - -### Для разработки: -```bash -# Запуск с отладкой -flutter run --debug - -# Сборка APK -flutter build apk --release - -# Тесты -flutter test - -# Анализ кода -flutter analyze -``` - ---- - -**Готово! Добавь SHA-1 и загружай правила! 🚀** - -Создано для команды NANAXE -Март 2026 diff --git a/lib/core/gitea_sync_service.dart b/lib/core/gitea_sync_service.dart new file mode 100644 index 0000000..b2b601f --- /dev/null +++ b/lib/core/gitea_sync_service.dart @@ -0,0 +1,321 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Модель файла из Gitea Contents API +class GiteaFile { + final String name; + final String path; + final String type; // 'file' | 'dir' + final String? sha; + final String? content; // base64 encoded + final int? size; + final String? downloadUrl; + + GiteaFile({ + required this.name, + required this.path, + required this.type, + this.sha, + this.content, + this.size, + this.downloadUrl, + }); + + factory GiteaFile.fromJson(Map json) { + return GiteaFile( + name: json['name'] ?? '', + path: json['path'] ?? '', + type: json['type'] ?? 'file', + sha: json['sha'], + content: json['content'], + size: json['size'], + downloadUrl: json['download_url'], + ); + } + + /// Декодирует base64 content (Gitea возвращает base64 с переносами) + String? get decodedContent { + if (content == null || content!.isEmpty) return null; + try { + final clean = content!.replaceAll('\n', ''); + return utf8.decode(base64Decode(clean)); + } catch (e) { + return null; + } + } +} + +/// Сервис прямой синхронизации с Gitea REST API +/// Используется в мобильном приложении вместо Obsidian Git plugin +class GiteaSyncService { + static final GiteaSyncService _instance = GiteaSyncService._internal(); + factory GiteaSyncService() => _instance; + GiteaSyncService._internal(); + + String? _baseUrl; + String? _token; + String? _owner; + String? _repo; + + static const String _prefsBaseUrlKey = 'gitea_base_url'; + static const String _prefsTokenKey = 'gitea_token'; + static const String _prefsOwnerKey = 'gitea_owner'; + static const String _prefsRepoKey = 'gitea_repo'; + + /// Инициализация из SharedPreferences + Future initialize() async { + final prefs = await SharedPreferences.getInstance(); + _baseUrl = prefs.getString(_prefsBaseUrlKey); + _token = prefs.getString(_prefsTokenKey); + _owner = prefs.getString(_prefsOwnerKey); + _repo = prefs.getString(_prefsRepoKey); + } + + /// Настройка подключения + Future configure({ + required String baseUrl, + required String token, + required String owner, + required String repo, + }) async { + _baseUrl = baseUrl.replaceAll(RegExp(r'/+$'), ''); + _token = token; + _owner = owner; + _repo = repo; + + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_prefsBaseUrlKey, _baseUrl!); + await prefs.setString(_prefsTokenKey, _token!); + await prefs.setString(_prefsOwnerKey, _owner!); + await prefs.setString(_prefsRepoKey, _repo!); + } + + bool get isConfigured => + _baseUrl != null && _token != null && _owner != null && _repo != null; + + // ==================== AUTH / TEST ==================== + + /// Проверка соединения и валидности токена + Future testConnection() async { + if (!isConfigured) return false; + try { + final response = await _get('/api/v1/user'); + return response.statusCode == 200; + } catch (e) { + return false; + } + } + + /// Получить информацию о текущем пользователе + Future?> getCurrentUser() async { + final response = await _get('/api/v1/user'); + if (response.statusCode == 200) { + return jsonDecode(response.body) as Map; + } + return null; + } + + // ==================== REPOSITORY CONTENTS ==================== + + /// Получить список файлов в директории репозитория + Future> listFiles({ + String path = '', + String branch = 'main', + }) async { + final response = await _get( + '/api/v1/repos/$_owner/$_repo/contents/$path?ref=$branch', + ); + + if (response.statusCode != 200) { + throw Exception( + 'Failed to list files: ${response.statusCode} ${response.body}'); + } + + final List data = jsonDecode(response.body); + return data + .map((e) => GiteaFile.fromJson(e as Map)) + .toList(); + } + + /// Получить содержимое одного файла + Future getFile({ + required String path, + String branch = 'main', + }) async { + final response = await _get( + '/api/v1/repos/$_owner/$_repo/contents/$path?ref=$branch', + ); + + if (response.statusCode != 200) { + throw Exception( + 'Failed to get file: ${response.statusCode} ${response.body}'); + } + + final data = jsonDecode(response.body) as Map; + return GiteaFile.fromJson(data); + } + + /// Обновить/создать файл в репозитории + Future updateFile({ + required String path, + required String content, + required String commitMessage, + String branch = 'main', + String? authorName, + String? authorEmail, + }) async { + // Получаем текущий sha если файл существует + String? currentSha; + try { + final existing = await getFile(path: path, branch: branch); + currentSha = existing.sha; + } catch (_) { + // файл не существует — создаём новый + } + + final body = { + 'content': base64Encode(utf8.encode(content)), + 'message': commitMessage, + 'branch': branch, + }; + + if (currentSha != null) { + body['sha'] = currentSha; + } + + if (authorName != null && authorEmail != null) { + body['author'] = { + 'name': authorName, + 'email': authorEmail, + }; + } + + final response = await _put( + '/api/v1/repos/$_owner/$_repo/contents/$path', + body: body, + ); + + if (response.statusCode != 200 && response.statusCode != 201) { + throw Exception( + 'Failed to update file: ${response.statusCode} ${response.body}'); + } + } + + /// Удалить файл из репозитория + Future deleteFile({ + required String path, + required String commitMessage, + String branch = 'main', + }) async { + final existing = await getFile(path: path, branch: branch); + + final response = await _delete( + '/api/v1/repos/$_owner/$_repo/contents/$path', + body: { + 'message': commitMessage, + 'sha': existing.sha, + 'branch': branch, + }, + ); + + if (response.statusCode != 200 && response.statusCode != 204) { + throw Exception( + 'Failed to delete file: ${response.statusCode} ${response.body}'); + } + } + + // ==================== GIT OPERATIONS ==================== + + /// Получить дерево файлов (рекурсивно) + Future> getTree({ + String branch = 'main', + bool recursive = true, + }) async { + final response = await _get( + '/api/v1/repos/$_owner/$_repo/git/trees/$branch?recursive=${recursive ? 1 : 0}', + ); + + if (response.statusCode != 200) { + throw Exception('Failed to get tree: ${response.statusCode}'); + } + + final data = jsonDecode(response.body) as Map; + final List tree = data['tree'] ?? []; + + return tree + .where((e) => e['type'] == 'blob') + .map((e) => GiteaFile( + name: e['path'].toString().split('/').last, + path: e['path'] ?? '', + type: 'file', + sha: e['sha'], + size: e['size'], + )) + .toList(); + } + + /// Получить последний коммит ветки + Future?> getLatestCommit( + {String branch = 'main'}) async { + final response = await _get( + '/api/v1/repos/$_owner/$_repo/commits?sha=$branch&limit=1', + ); + + if (response.statusCode == 200) { + final List data = jsonDecode(response.body); + if (data.isNotEmpty) return data.first as Map; + } + return null; + } + + /// Получить историю коммитов + Future>> getCommits({ + String branch = 'main', + int page = 1, + int limit = 30, + }) async { + final response = await _get( + '/api/v1/repos/$_owner/$_repo/commits?sha=$branch&page=$page&limit=$limit', + ); + + if (response.statusCode == 200) { + final List data = jsonDecode(response.body); + return data.cast>(); + } + return []; + } + + // ==================== HELPERS ==================== + + Map _getHeaders() { + return { + 'Content-Type': 'application/json', + 'Authorization': 'token $_token', + }; + } + + Future _get(String path) async { + final uri = Uri.parse('$_baseUrl$path'); + return http.get(uri, headers: _getHeaders()); + } + + Future _put(String path, + {required Map body}) async { + final uri = Uri.parse('$_baseUrl$path'); + return http.put( + uri, + headers: _getHeaders(), + body: jsonEncode(body), + ); + } + + Future _delete(String path, + {required Map body}) async { + final uri = Uri.parse('$_baseUrl$path'); + final request = http.Request('DELETE', uri) + ..headers.addAll(_getHeaders()) + ..body = jsonEncode(body); + return http.Client().send(request).then((r) => http.Response.fromStream(r)); + } +} diff --git a/lib/core/markdown_mindmap_parser.dart b/lib/core/markdown_mindmap_parser.dart new file mode 100644 index 0000000..22eb3b0 --- /dev/null +++ b/lib/core/markdown_mindmap_parser.dart @@ -0,0 +1,194 @@ +import 'package:uuid/uuid.dart'; +import '../models/mind_map_model.dart'; + +/// Парсер Markdown-файлов в MindMapNode структуру +/// +/// Ожидаемая структура markdown: +/// ```markdown +/// --- +/// title: Название карты +/// tags: [crm, project] +/// --- +/// +/// # Корневой узел +/// Описание корневого узла +/// +/// ## Дочерний узел 1 +/// Описание +/// +/// ### Внук 1 +/// +/// ## Дочерний узел 2 +/// ``` +class MarkdownMindmapParser { + static const Uuid _uuid = Uuid(); + + /// Парсит markdown строку в дерево MindMapNode + /// Возвращает корневой узел + все дочерние + static List parse(String markdown, String projectId) { + final lines = markdown.split('\n'); + final nodes = []; + final now = DateTime.now(); + + String? currentParentId; + final Map levelToParentId = {}; + + for (final line in lines) { + final trimmed = line.trim(); + if (trimmed.isEmpty) continue; + + // Пропускаем frontmatter + if (trimmed == '---') continue; + if (trimmed.startsWith('title:') || + trimmed.startsWith('tags:') || + trimmed.startsWith('date:')) { + continue; + } + + // Определяем уровень заголовка + final headerMatch = _headerRegex.firstMatch(trimmed); + if (headerMatch != null) { + final hashes = headerMatch.group(1)!; + final level = hashes.length; + final title = trimmed.substring(hashes.length).trim(); + + final id = _uuid.v4(); + + // Определяем parentId на основе уровня + if (level == 1) { + // Корневой узел + currentParentId = null; + levelToParentId.clear(); + } else { + // Ищем ближайший родитель с уровнем меньше текущего + String? parentId; + for (int l = level - 1; l >= 1; l--) { + if (levelToParentId.containsKey(l)) { + parentId = levelToParentId[l]; + break; + } + } + currentParentId = parentId; + } + + levelToParentId[level] = id; + + nodes.add(MindMapNode( + id: id, + projectId: projectId, + parentId: currentParentId, + level: level, + title: title, + color: _levelColor(level), + createdAt: now, + updatedAt: now, + )); + } + } + + // Если нет ни одного заголовка — создаём один корневой узел с текстом + if (nodes.isEmpty && markdown.trim().isNotEmpty) { + final firstLine = markdown.trim().split('\n').first.trim(); + nodes.add(MindMapNode( + id: _uuid.v4(), + projectId: projectId, + parentId: null, + level: 0, + title: firstLine.length > 50 ? '${firstLine.substring(0, 50)}...' : firstLine, + color: _levelColor(0), + createdAt: now, + updatedAt: now, + )); + } + + return nodes; + } + + /// Конвертирует дерево MindMapNode обратно в Markdown + static String toMarkdown(List nodes, {String? title}) { + if (nodes.isEmpty) return ''; + + final buffer = StringBuffer(); + + // Frontmatter + buffer.writeln('---'); + buffer.writeln('title: ${title ?? 'Mindmap'}'); + buffer.writeln('date: ${DateTime.now().toIso8601String()}'); + buffer.writeln('---'); + buffer.writeln(); + + // Сортируем по уровню, затем по порядку + final sorted = List.from(nodes) + ..sort((a, b) { + final levelCompare = a.level.compareTo(b.level); + if (levelCompare != 0) return levelCompare; + return a.createdAt.compareTo(b.createdAt); + }); + + for (final node in sorted) { + final hashes = '#' * node.level.clamp(1, 6); + buffer.writeln('$hashes ${node.title}'); + if (node.description != null && node.description!.isNotEmpty) { + buffer.writeln(node.description); + } + buffer.writeln(); + } + + return buffer.toString().trim(); + } + + /// Извлекает plain text описание из markdown (без заголовков) + static String? extractDescription(String markdown) { + final lines = markdown.split('\n'); + final buffer = StringBuffer(); + bool inFrontmatter = false; + + for (final line in lines) { + final trimmed = line.trim(); + + if (trimmed == '---') { + inFrontmatter = !inFrontmatter; + continue; + } + if (inFrontmatter) continue; + if (trimmed.startsWith('#')) continue; + + if (trimmed.isNotEmpty) { + buffer.writeln(trimmed); + } + } + + final result = buffer.toString().trim(); + return result.isEmpty ? null : result; + } + + /// Извлекает wiki-ссылки [[Link]] из текста + static List extractWikiLinks(String text) { + final matches = _wikiLinkRegex.allMatches(text); + return matches.map((m) => m.group(1)!).toList(); + } + + // ==================== PRIVATE ==================== + + static final RegExp _headerRegex = RegExp(r'^(#{1,6})\s+(.+)$'); + static final RegExp _wikiLinkRegex = RegExp(r'\[\[([^\]]+)\]\]'); + + static String _levelColor(int level) { + switch (level) { + case 0: + return '#E53935'; // red + case 1: + return '#1E88E5'; // blue + case 2: + return '#43A047'; // green + case 3: + return '#FB8C00'; // orange + case 4: + return '#8E24AA'; // purple + case 5: + return '#00ACC1'; // cyan + default: + return '#78909C'; // grey + } + } +} diff --git a/lib/core/offline_mindmap_repository.dart b/lib/core/offline_mindmap_repository.dart new file mode 100644 index 0000000..29f673d --- /dev/null +++ b/lib/core/offline_mindmap_repository.dart @@ -0,0 +1,180 @@ +import 'dart:convert'; +import 'package:hive/hive.dart'; + +/// Hive модель для кэшированного mindmap-файла +class CachedMindmap { + final String path; + final String content; + final String? sha; + final DateTime lastSync; + final DateTime lastModified; + final String branch; + + CachedMindmap({ + required this.path, + required this.content, + this.sha, + required this.lastSync, + required this.lastModified, + this.branch = 'main', + }); + + Map toJson() => { + 'path': path, + 'content': content, + 'sha': sha, + 'lastSync': lastSync.toIso8601String(), + 'lastModified': lastModified.toIso8601String(), + 'branch': branch, + }; + + factory CachedMindmap.fromJson(Map json) { + return CachedMindmap( + path: json['path'] ?? '', + content: json['content'] ?? '', + sha: json['sha'], + lastSync: + DateTime.parse(json['lastSync'] ?? DateTime.now().toIso8601String()), + lastModified: DateTime.parse( + json['lastModified'] ?? DateTime.now().toIso8601String()), + branch: json['branch'] ?? 'main', + ); + } +} + +/// Оффлайн репозиторий для mindmaps +/// Хранит кэш файлов из Gitea и очередь изменений для синхронизации +class OfflineMindmapRepository { + static const String _boxName = 'mindmaps_cache'; + static const String _queueBoxName = 'mindmaps_queue'; + static const String _metaKey = '_meta_sync_state'; + + Box? _box; + Box? _queueBox; + + Future initialize() async { + _box = await Hive.openBox(_boxName); + _queueBox = await Hive.openBox(_queueBoxName); + } + + bool get isInitialized => _box != null && _box!.isOpen; + + // ==================== CACHE ==================== + + /// Сохранить файл в кэш + Future cacheMindmap(CachedMindmap mindmap) async { + if (!isInitialized) throw Exception('Repository not initialized'); + await _box!.put(mindmap.path, jsonEncode(mindmap.toJson())); + } + + /// Получить файл из кэша + CachedMindmap? getMindmap(String path) { + if (!isInitialized) return null; + final raw = _box!.get(path); + if (raw == null) return null; + try { + return CachedMindmap.fromJson(jsonDecode(raw)); + } catch (_) { + return null; + } + } + + /// Получить все кэшированные mindmaps + List getAllMindmaps() { + if (!isInitialized) return []; + return _box!.values + .map((raw) { + try { + return CachedMindmap.fromJson(jsonDecode(raw)); + } catch (_) { + return null; + } + }) + .whereType() + .toList(); + } + + /// Удалить файл из кэша + Future deleteMindmap(String path) async { + if (!isInitialized) return; + await _box!.delete(path); + } + + /// Очистить весь кэш + Future clearCache() async { + if (!isInitialized) return; + await _box!.clear(); + } + + // ==================== SYNC QUEUE ==================== + + /// Добавить изменение в очередь (для offline → online sync) + Future queueChange({ + required String path, + required String content, + required String operation, // 'create' | 'update' | 'delete' + String? commitMessage, + }) async { + if (!isInitialized) throw Exception('Repository not initialized'); + final entry = { + 'path': path, + 'content': content, + 'operation': operation, + 'commitMessage': commitMessage ?? 'Mobile sync: $path', + 'timestamp': DateTime.now().toIso8601String(), + }; + await _queueBox!.put( + '${DateTime.now().millisecondsSinceEpoch}_$path', jsonEncode(entry)); + } + + /// Получить все изменения из очереди + List> getPendingChanges() { + if (!isInitialized) return []; + return _queueBox!.values + .map((raw) { + try { + return jsonDecode(raw) as Map; + } catch (_) { + return null; + } + }) + .whereType>() + .toList() + ..sort((a, b) => (a['timestamp'] ?? '').compareTo(b['timestamp'] ?? '')); + } + + /// Удалить выполненное изменение из очереди + Future removeQueuedChange(String key) async { + if (!isInitialized) return; + await _queueBox!.delete(key); + } + + /// Очистить очередь + Future clearQueue() async { + if (!isInitialized) return; + await _queueBox!.clear(); + } + + bool get hasPendingChanges => (_queueBox?.length ?? 0) > 0; + + // ==================== SYNC STATE ==================== + + /// Сохранить время последней успешной синхронизации + Future setLastSync(DateTime time) async { + if (!isInitialized) return; + await _box!.put(_metaKey, jsonEncode({'lastSync': time.toIso8601String()})); + } + + /// Получить время последней синхронизации + DateTime? getLastSync() { + if (!isInitialized) return null; + final raw = _box!.get(_metaKey); + if (raw == null) return null; + try { + final data = jsonDecode(raw) as Map; + return DateTime.tryParse(data['lastSync'] ?? ''); + } catch (_) { + return null; + } + } +} diff --git a/lib/core/router.dart b/lib/core/router.dart index 6ea3b88..03c4dd1 100644 --- a/lib/core/router.dart +++ b/lib/core/router.dart @@ -17,6 +17,8 @@ import '../screens/admin/projects_management_screen.dart'; import '../screens/admin/notifications_settings_screen.dart'; import '../screens/admin/mind_map_admin_screen.dart'; import '../screens/admin/ai_stats_screen.dart'; +import '../screens/gitea_sync_settings_screen.dart'; +import '../screens/gitea_mindmaps_list_screen.dart'; import '../providers/auth_provider.dart'; // Глобальный ключ для навигации @@ -81,6 +83,16 @@ class AppRouter { return MindMapSettingsScreen(projectId: projectId); }, ), + GoRoute( + path: 'gitea-sync', + name: 'gitea-sync', + builder: (context, state) => const GiteaSyncSettingsScreen(), + ), + GoRoute( + path: 'gitea-mindmaps', + name: 'gitea-mindmaps', + builder: (context, state) => const GiteaMindmapsListScreen(), + ), ], ), GoRoute( diff --git a/lib/main.dart b/lib/main.dart index cef558a..e2375ac 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,8 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'core/firebase_config.dart'; import 'core/local_storage_service.dart'; import 'core/notification_service.dart'; +import 'core/gitea_sync_service.dart'; +import 'core/offline_mindmap_repository.dart'; import 'core/theme.dart'; import 'core/router.dart'; @@ -22,6 +24,10 @@ void main() async { // Инициализация уведомлений await notificationService.initialize(); + // Инициализация Gitea синхронизации (mindmaps offline cache) + await GiteaSyncService().initialize(); + await OfflineMindmapRepository().initialize(); + runApp( const ProviderScope( child: NanaxeCrmApp(), diff --git a/lib/providers/gitea_sync_provider.dart b/lib/providers/gitea_sync_provider.dart new file mode 100644 index 0000000..fb541b1 --- /dev/null +++ b/lib/providers/gitea_sync_provider.dart @@ -0,0 +1,228 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../core/gitea_sync_service.dart'; +import '../core/offline_mindmap_repository.dart'; +import '../core/markdown_mindmap_parser.dart'; +import '../models/mind_map_model.dart'; + +/// Provider для GiteaSyncService +final giteaSyncServiceProvider = Provider((ref) { + return GiteaSyncService(); +}); + +/// Provider для OfflineMindmapRepository +final offlineMindmapRepoProvider = Provider((ref) { + return OfflineMindmapRepository(); +}); + +/// State для mindmap синхронизации +class MindmapSyncState { + final List files; + final List cachedMindmaps; + final bool isLoading; + final bool isSyncing; + final String? error; + final bool isConfigured; + + MindmapSyncState({ + this.files = const [], + this.cachedMindmaps = const [], + this.isLoading = false, + this.isSyncing = false, + this.error, + this.isConfigured = false, + }); + + MindmapSyncState copyWith({ + List? files, + List? cachedMindmaps, + bool? isLoading, + bool? isSyncing, + String? error, + bool? isConfigured, + }) { + return MindmapSyncState( + files: files ?? this.files, + cachedMindmaps: cachedMindmaps ?? this.cachedMindmaps, + isLoading: isLoading ?? this.isLoading, + isSyncing: isSyncing ?? this.isSyncing, + error: error ?? this.error, + isConfigured: isConfigured ?? this.isConfigured, + ); + } +} + +/// Notifier для управления синхронизацией mindmaps +class MindmapSyncNotifier extends StateNotifier { + final GiteaSyncService _gitea; + final OfflineMindmapRepository _repo; + + MindmapSyncNotifier(this._gitea, this._repo) : super(MindmapSyncState()); + + /// Инициализация сервиса + Future initialize() async { + await _repo.initialize(); + await _gitea.initialize(); + state = state.copyWith(isConfigured: _gitea.isConfigured); + + if (state.isConfigured) { + await loadCachedMindmaps(); + } + } + + /// Настройка Gitea + Future configure({ + required String baseUrl, + required String token, + required String owner, + required String repo, + }) async { + state = state.copyWith(isLoading: true, error: null); + try { + await _gitea.configure( + baseUrl: baseUrl, + token: token, + owner: owner, + repo: repo, + ); + final connected = await _gitea.testConnection(); + if (!connected) { + throw Exception('Failed to connect to Gitea. Check token and URL.'); + } + state = state.copyWith(isConfigured: true, isLoading: false); + await syncFromGitea(); + } catch (e) { + state = state.copyWith(isLoading: false, error: e.toString()); + } + } + + /// Загрузить mindmaps из кэша + Future loadCachedMindmaps() async { + final cached = _repo.getAllMindmaps(); + state = state.copyWith(cachedMindmaps: cached); + } + + /// Полная синхронизация с Gitea (download) + Future syncFromGitea({String branch = 'main'}) async { + if (!state.isConfigured) return; + + state = state.copyWith(isSyncing: true, error: null); + try { + // Получаем дерево файлов + final files = await _gitea.getTree(branch: branch); + final mindmapFiles = files.where((f) => + f.path.endsWith('.md') || f.path.endsWith('.canvas')).toList(); + + // Загружаем каждый файл + for (final file in mindmapFiles) { + try { + final giteaFile = await _gitea.getFile(path: file.path, branch: branch); + final content = giteaFile.decodedContent; + if (content != null) { + await _repo.cacheMindmap(CachedMindmap( + path: file.path, + content: content, + sha: giteaFile.sha, + lastSync: DateTime.now(), + lastModified: DateTime.now(), + branch: branch, + )); + } + } catch (e) { + // Логируем ошибку но продолжаем + print('Failed to sync ${file.path}: $e'); + } + } + + await loadCachedMindmaps(); + state = state.copyWith(isSyncing: false); + } catch (e) { + state = state.copyWith(isSyncing: false, error: e.toString()); + } + } + + /// Парсить markdown-кэш в MindMapNode + List? parseMindmap(String path) { + final cached = _repo.getMindmap(path); + if (cached == null) return null; + + return MarkdownMindmapParser.parse( + cached.content, + path.replaceAll('/', '_'), + ); + } + + /// Сохранить изменения mindmap и запушить в Gitea + Future saveAndPush({ + required String path, + required List nodes, + String? title, + String branch = 'main', + }) async { + if (!state.isConfigured) { + throw Exception('Gitea not configured'); + } + + final markdown = MarkdownMindmapParser.toMarkdown(nodes, title: title); + + // Сохраняем локально + await _repo.cacheMindmap(CachedMindmap( + path: path, + content: markdown, + lastSync: DateTime.now(), + lastModified: DateTime.now(), + branch: branch, + )); + + // Пушим в Gitea + try { + await _gitea.updateFile( + path: path, + content: markdown, + commitMessage: 'mobile: update $path', + branch: branch, + authorName: 'NANAXE Mobile', + authorEmail: 'mobile@nanaxe.local', + ); + } catch (e) { + // Если нет сети — кладём в очередь + await _repo.queueChange( + path: path, + content: markdown, + operation: 'update', + commitMessage: 'mobile: update $path', + ); + } + + await loadCachedMindmaps(); + } + + /// Отправить накопленные offline-изменения + Future pushPendingChanges({String branch = 'main'}) async { + if (!state.isConfigured) return; + + final pending = _repo.getPendingChanges(); + for (final change in pending) { + try { + await _gitea.updateFile( + path: change['path'], + content: change['content'], + commitMessage: change['commitMessage'] ?? 'Mobile sync', + branch: branch, + ); + // Удаляем из очереди по timestamp_path + final key = '${change['timestamp']}_${change['path']}'; + await _repo.removeQueuedChange(key); + } catch (e) { + print('Failed to push queued change: $e'); + } + } + } +} + +/// StateNotifierProvider для mindmap синхронизации +final mindmapSyncProvider = + StateNotifierProvider((ref) { + final gitea = ref.watch(giteaSyncServiceProvider); + final repo = ref.watch(offlineMindmapRepoProvider); + return MindmapSyncNotifier(gitea, repo); +}); diff --git a/lib/screens/gitea_mindmaps_list_screen.dart b/lib/screens/gitea_mindmaps_list_screen.dart new file mode 100644 index 0000000..8fc81f6 --- /dev/null +++ b/lib/screens/gitea_mindmaps_list_screen.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../providers/gitea_sync_provider.dart'; +import '../core/markdown_mindmap_parser.dart'; + +class GiteaMindmapsListScreen extends ConsumerWidget { + const GiteaMindmapsListScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(mindmapSyncProvider); + final notifier = ref.read(mindmapSyncProvider.notifier); + + return Scaffold( + appBar: AppBar( + title: const Text('Mindmaps из Gitea'), + actions: [ + IconButton( + icon: const Icon(Icons.settings), + onPressed: () => context.pushNamed('gitea-sync'), + ), + if (state.isConfigured) + IconButton( + icon: state.isSyncing + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : const Icon(Icons.sync), + onPressed: state.isSyncing ? null : () => notifier.syncFromGitea(), + ), + ], + ), + body: _buildBody(context, state, notifier), + ); + } + + Widget _buildBody(BuildContext context, MindmapSyncState state, MindmapSyncNotifier notifier) { + if (!state.isConfigured) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.cloud_off, size: 64, color: Colors.grey), + const SizedBox(height: 16), + const Text('Gitea не настроен'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => context.pushNamed('gitea-sync'), + child: const Text('Настроить'), + ), + ], + ), + ); + } + + if (state.isSyncing && state.cachedMindmaps.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.error != null && state.cachedMindmaps.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 64, color: Colors.red), + const SizedBox(height: 16), + Text('Ошибка: ${state.error}'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => notifier.syncFromGitea(), + child: const Text('Повторить'), + ), + ], + ), + ); + } + + final mindmaps = state.cachedMindmaps; + + if (mindmaps.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.folder_open, size: 64, color: Colors.grey), + const SizedBox(height: 16), + const Text('Нет mindmaps в кэше'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => notifier.syncFromGitea(), + child: const Text('Синхронизировать с Gitea'), + ), + ], + ), + ); + } + + return ListView.builder( + itemCount: mindmaps.length, + itemBuilder: (context, index) { + final mm = mindmaps[index]; + final nodes = MarkdownMindmapParser.parse(mm.content, mm.path); + final rootTitle = nodes.isNotEmpty ? nodes.first.title : mm.path; + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: ListTile( + leading: CircleAvatar( + backgroundColor: _getColorForPath(mm.path), + child: const Icon(Icons.account_tree, color: Colors.white), + ), + title: Text(rootTitle), + subtitle: Text( + '${mm.path}\n' + '${mm.lastSync.toLocal().toString().split('.').first}', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + isThreeLine: true, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (mm.path.endsWith('.canvas')) + const Chip( + label: Text('Canvas', style: TextStyle(fontSize: 10)), + padding: EdgeInsets.zero, + ), + const Icon(Icons.chevron_right), + ], + ), + onTap: () { + // TODO: открыть просмотр mindmap + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Открытие ${mm.path}...')), + ); + }, + ), + ); + }, + ); + } + + Color _getColorForPath(String path) { + if (path.endsWith('.canvas')) return Colors.purple; + if (path.endsWith('.md')) return Colors.blue; + return Colors.grey; + } +} diff --git a/lib/screens/gitea_sync_settings_screen.dart b/lib/screens/gitea_sync_settings_screen.dart new file mode 100644 index 0000000..270305a --- /dev/null +++ b/lib/screens/gitea_sync_settings_screen.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import '../providers/gitea_sync_provider.dart'; + +class GiteaSyncSettingsScreen extends ConsumerStatefulWidget { + const GiteaSyncSettingsScreen({super.key}); + + @override + ConsumerState createState() => _GiteaSyncSettingsScreenState(); +} + +class _GiteaSyncSettingsScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _urlCtrl = TextEditingController(text: 'http://git.a-crm.controlsystem.kz'); + final _tokenCtrl = TextEditingController(); + final _ownerCtrl = TextEditingController(text: 'Alexyz'); + final _repoCtrl = TextEditingController(text: 'CMMNANAXE-CRM'); + + bool _testing = false; + + @override + void dispose() { + _urlCtrl.dispose(); + _tokenCtrl.dispose(); + _ownerCtrl.dispose(); + _repoCtrl.dispose(); + super.dispose(); + } + + Future _testAndSave() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _testing = true); + + final notifier = ref.read(mindmapSyncProvider.notifier); + try { + await notifier.configure( + baseUrl: _urlCtrl.text.trim(), + token: _tokenCtrl.text.trim(), + owner: _ownerCtrl.text.trim(), + repo: _repoCtrl.text.trim(), + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Подключение к Gitea установлено')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ошибка: $e'), backgroundColor: Colors.red), + ); + } + } finally { + if (mounted) setState(() => _testing = false); + } + } + + Future _syncNow() async { + setState(() => _testing = true); + final notifier = ref.read(mindmapSyncProvider.notifier); + try { + await notifier.syncFromGitea(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Синхронизация завершена')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Ошибка синхронизации: $e'), backgroundColor: Colors.red), + ); + } + } finally { + if (mounted) setState(() => _testing = false); + } + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(mindmapSyncProvider); + + return Scaffold( + appBar: AppBar( + title: const Text('Синхронизация с Gitea'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.pop(), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: ListView( + children: [ + // Статус + Card( + child: ListTile( + leading: Icon( + state.isConfigured ? Icons.cloud_done : Icons.cloud_off, + color: state.isConfigured ? Colors.green : Colors.grey, + ), + title: Text( + state.isConfigured ? 'Подключено к Gitea' : 'Не подключено', + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: state.isSyncing + ? const Text('Синхронизация...') + : state.error != null + ? Text('Ошибка: ${state.error}') + : const Text('Настройте подключение ниже'), + ), + ), + const SizedBox(height: 16), + + // URL + TextFormField( + controller: _urlCtrl, + decoration: const InputDecoration( + labelText: 'Gitea URL', + hintText: 'http://git.a-crm.controlsystem.kz', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.link), + ), + validator: (v) => v == null || v.isEmpty ? 'Укажите URL' : null, + ), + const SizedBox(height: 12), + + // Token + TextFormField( + controller: _tokenCtrl, + decoration: const InputDecoration( + labelText: 'Access Token', + hintText: 'Ваш токен из Gitea (Settings → Applications)', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.key), + ), + obscureText: true, + validator: (v) => v == null || v.isEmpty ? 'Укажите токен' : null, + ), + const SizedBox(height: 12), + + // Owner + TextFormField( + controller: _ownerCtrl, + decoration: const InputDecoration( + labelText: 'Owner (пользователь/организация)', + hintText: 'Alexyz', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.person), + ), + validator: (v) => v == null || v.isEmpty ? 'Укажите владельца' : null, + ), + const SizedBox(height: 12), + + // Repo + TextFormField( + controller: _repoCtrl, + decoration: const InputDecoration( + labelText: 'Repository', + hintText: 'CMMNANAXE-CRM', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.folder), + ), + validator: (v) => v == null || v.isEmpty ? 'Укажите репозиторий' : null, + ), + const SizedBox(height: 24), + + // Кнопки + ElevatedButton.icon( + onPressed: _testing ? null : _testAndSave, + icon: _testing + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.save), + label: const Text('Сохранить и проверить'), + ), + const SizedBox(height: 12), + + if (state.isConfigured) + ElevatedButton.icon( + onPressed: _testing ? null : _syncNow, + icon: const Icon(Icons.sync), + label: const Text('Синхронизировать сейчас'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/server/DEPLOY.md b/server/DEPLOY.md deleted file mode 100644 index 120e6d6..0000000 --- a/server/DEPLOY.md +++ /dev/null @@ -1,369 +0,0 @@ -# 🚀 Развёртывание Nanaxe Server на Proxmox VPS - -## 📋 Содержание -1. [Подготовка VPS](#подготовка-vps) -2. [Установка Docker](#установка-docker) -3. [Настройка сервера](#настройка-сервера) -4. [Запуск](#запуск) -5. [Настройка домена и SSL](#настройка-домена-и-ssl) -6. [Подключение Flutter-приложения](#подключение-flutter-приложения) - ---- - -## Подготовка VPS - -### 1. Создание LXC контейнера в Proxmox - -```bash -# На хосте Proxmox создайте контейнер -pct create 100 local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst \ - --hostname nanaxe-server \ - --memory 2048 \ - --cores 2 \ - --rootfs local-lvm:10 \ - --net0 name=eth0,bridge=vmbr0,ip=dhcp - -# Запустите контейнер -pct start 100 - -# Войдите в контейнер -pct enter 100 -``` - -### 2. Обновление системы - -```bash -apt update && apt upgrade -y -apt install -y curl wget git nano htop -``` - ---- - -## Установка Docker - -```bash -# Установка Docker -curl -fsSL https://get.docker.com -o get-docker.sh -sh get-docker.sh - -# Добавление пользователя в группу docker -usermod -aG docker $USER - -# Установка Docker Compose -apt install -y docker-compose-plugin - -# Проверка -docker --version -docker compose version -``` - ---- - -## Настройка сервера - -### 1. Клонирование репозитория - -```bash -cd /opt -git clone nanaxe-server -cd nanaxe-server/server -``` - -### 2. Настройка переменных окружения - -```bash -cp .env.example .env -nano .env -``` - -Заполните файл: - -```env -# Domain configuration -GITEA_DOMAIN=your-domain.com - -# Gitea Configuration -GITEA_TOKEN=your_gitea_api_token_here - -# Security (сгенерируйте случайный ключ) -JWT_SECRET=$(openssl rand -base64 32) - -# WebDAV (optional) -WEBDAV_USER=admin -WEBDAV_PASSWORD=your_secure_password - -# Database (обязательно измените!) -POSTGRES_PASSWORD=$(openssl rand -base64 24) -``` - -### 3. Создание директорий - -```bash -mkdir -p api/logs -mkdir -p nginx/html -``` - ---- - -## Запуск - -### 1. Первый запуск - -```bash -docker compose up -d -``` - -### 2. Проверка статуса - -```bash -docker compose ps -docker compose logs -f -``` - -### 3. Инициализация Gitea - -Откройте `http://your-server-ip:3000` и выполните первоначальную настройку: - -1. Установите базу данных (уже настроена PostgreSQL) -2. Создайте администратора -3. Создайте API токен в Settings → Applications -4. Добавьте токен в `.env` файл - -```bash -# Перезапустите сервисы -docker compose restart -``` - ---- - -## Настройка домена и SSL - -### 1. Настройка DNS - -Укажите A-записи: -- `your-domain.com` → IP сервера -- `git.your-domain.com` → IP сервера -- `api.your-domain.com` → IP сервера - -### 2. Установка Certbot - -```bash -apt install -y certbot - -# Получение сертификатов -certbot certonly --standalone -d your-domain.com -d git.your-domain.com -d api.your-domain.com -``` - -### 3. Настройка Nginx - -Отредактируйте `nginx/nginx.conf`: - -```nginx -# Раскомментируйте HTTPS server section -server { - listen 443 ssl http2; - server_name your-domain.com; - - ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; - - # ... rest of config -} -``` - -### 4. Обновление docker-compose.yml - -Добавьте volume для SSL сертификатов: - -```yaml -nginx: - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/html:/usr/share/nginx/html:ro - - /etc/letsencrypt:/etc/letsencrypt:ro # Добавьте это -``` - -### 5. Перезапуск - -```bash -docker compose down -docker compose up -d -``` - ---- - -## Подключение Flutter-приложения - -### 1. Настройка сервера в приложении - -```dart -import 'package:nanaxe_crm/core/nanaxe_server_service.dart'; - -final serverService = NanaxeServerService(); - -// При инициализации -await serverService.initialize(); - -// Если не настроен - настройте -if (!serverService.isConfigured) { - await serverService.configure( - 'https://api.your-domain.com', - ); -} - -// Проверка соединения -final isConnected = await serverService.checkConnection(); -``` - -### 2. Авторизация - -```dart -// Регистрация -await serverService.register( - 'user@example.com', - 'password', - 'username', -); - -// Или вход -await serverService.login( - 'user@example.com', - 'password', -); -``` - -### 3. Работа с mindmaps - -```dart -// Получение списка -final mindmaps = await serverService.getMindMaps(); - -// Создание -final newMap = await serverService.createMindMap( - title: 'My Project', - description: 'Project planning', - nodes: [ - {'id': '1', 'title': 'Root', 'level': 0, 'parentId': null}, - {'id': '2', 'title': 'Task 1', 'level': 1, 'parentId': '1'}, - ], -); - -// Синхронизация с Gitea -await serverService.pushToGitea(commitMessage: 'Updated project plan'); -``` - ---- - -## 🔧 Обслуживание - -### Обновление - -```bash -cd /opt/nanaxe-server/server -git pull -docker compose pull -docker compose up -d -``` - -### Бэкап - -```bash -# Бэкап данных -tar -czf backup-$(date +%Y%m%d).tar.gz /opt/nanaxe-server/server - -# Или бэкап volumes -docker run --rm -v nanaxe-server_mindmap-data:/data -v $(pwd):/backup alpine tar czf /backup/mindmaps-backup.tar.gz -C /data . -``` - -### Мониторинг - -```bash -# Логи -docker compose logs -f - -# Статистика -docker stats - -# Дисковое пространство -df -h -docker system df -``` - ---- - -## 🌐 Доступ к сервисам - -После развёртывания: - -| Сервис | URL | Описание | -|--------|-----|----------| -| Web UI | `http://your-domain.com` | Главная страница | -| Gitea | `http://your-domain.com:3000` | Git репозиторий | -| API | `http://your-domain.com:8080` | REST API | -| WebDAV | `http://your-domain.com:8081` | Прямой доступ к файлам | - ---- - -## 📱 Obsidian Интеграция - -### 1. Git Sync плагин - -Установите плагин **Obsidian Git** и настройте: - -```bash -# В настройках плагина -Remote URL: http://your-domain.com:3000/username/mindmaps.git -``` - -### 2. Прямой доступ через WebDAV - -Установите плагин **Remote Save** и настройте WebDAV: - -``` -URL: http://your-domain.com:8081 -Username: admin -Password: your_secure_password -``` - ---- - -## 🆘 Устранение неполадок - -### Проблема: Порты заняты - -```bash -# Проверка занятых портов -netstat -tlnp | grep -E '3000|8080|80' - -# Измените порты в docker-compose.yml -``` - -### Проблема: Permission denied - -```bash -# Проверьте права -ls -la /opt/nanaxe-server/server - -# Исправьте -chown -R $USER:$USER /opt/nanaxe-server/server -``` - -### Проблема: Контейнеры не запускаются - -```bash -# Проверьте логи -docker compose logs - -# Пересоберите -docker compose down -docker compose build --no-cache -docker compose up -d -``` - ---- - -## 📚 Дополнительно - -- [Gitea Documentation](https://docs.gitea.io/) -- [Docker Documentation](https://docs.docker.com/) -- [Nginx Documentation](https://nginx.org/en/docs/) diff --git a/server/INTEGRATION_GUIDE.md b/server/INTEGRATION_GUIDE.md deleted file mode 100644 index 40903aa..0000000 --- a/server/INTEGRATION_GUIDE.md +++ /dev/null @@ -1,164 +0,0 @@ -# 🔗 Интеграция Nanaxe Server с Consyscab - -Это руководство по добавлению Nanaxe Server в существующую инфраструктуру Consyscab. - -## 📁 Структура файлов - -``` -project-root/ -├── docker-compose.yml # Объединённый файл -├── docker/ -│ ├── nginx/ -│ │ └── conf.d/ -│ │ ├── default.conf # Существующий конфиг -│ │ └── nanaxe.conf # Новый: конфиг Nanaxe -│ ├── php/ -│ └── postgres/ -│ └── init-db.sh -├── nanaxe/ # Новая папка -│ ├── api/ # Nanaxe API код -│ │ ├── Dockerfile -│ │ ├── package.json -│ │ └── src/ -│ ├── nginx/ -│ │ └── nanaxe.conf # Nginx конфиг для Nanaxe -│ ├── html/ # Статические файлы -│ │ └── index.html -│ └── postgres-init/ -│ └── init-gitea.sql # Инициализация БД Gitea -├── backend/ # Symfony -├── frontend/ # Quasar -├── moodle/ # Moodle -└── .env # Переменные окружения -``` - -## 🚀 Быстрый старт - -### 1. Скопируйте файлы Nanaxe - -```bash -# Скопируйте папку nanaxe/ в корень проекта вашего друга -cp -r /path/to/nanaxe/server/nanaxe ./ - -# Скопируйте объединённый docker-compose.yml -cp /path/to/nanaxe/server/docker-compose.integrated.yml ./docker-compose.yml -``` - -### 2. Обновите .env - -Добавьте в `.env`: - -```env -# Nanaxe Configuration -GITEA_DOMAIN=git.yourdomain.com -GITEA_SECRET_KEY=$(openssl rand -base64 32) -GITEA_TOKEN= -NANAXE_JWT_SECRET=$(openssl rand -base64 32) -WEBDAV_USER=admin -WEBDAV_PASSWORD=$(openssl rand -base64 16) -DB_PASSWORD=your_postgres_password -``` - -### 3. Настройте DNS - -Настройте поддомены: -- `git.yourdomain.com` → IP сервера -- `api.yourdomain.com` → IP сервера -- `nanaxe.yourdomain.com` → IP сервера (опционально) - -### 4. Запустите - -```bash -docker compose down -docker compose up -d --build -``` - -## 🔧 Ручная интеграция (если не хотите заменять docker-compose.yml) - -### Добавьте сервисы в существующий docker-compose.yml: - -```yaml - # Nanaxe Gitea - nanaxe_gitea: - image: gitea/gitea:latest - container_name: nanaxe_gitea - environment: - - GITEA__database__DB_TYPE=postgres - - GITEA__database__HOST=consyscab_db:5432 - - GITEA__database__NAME=gitea - - GITEA__database__USER=user - - GITEA__database__PASSWD=${DB_PASSWORD} - volumes: - - gitea_data:/data - networks: - - consyscab-network - - # Nanaxe API - nanaxe_api: - build: - context: ./nanaxe/api - container_name: nanaxe_api - environment: - - NODE_ENV=production - - PORT=8080 - - GITEA_URL=http://nanaxe_gitea:3000 - - JWT_SECRET=${NANAXE_JWT_SECRET} - volumes: - - mindmap_data:/app/mindmaps - networks: - - consyscab-network - -volumes: - pg_data: - gitea_data: # Добавьте - mindmap_data: # Добавьте -``` - -### Добавьте в nginx конфиг: - -Создайте файл `docker/nginx/conf.d/nanaxe.conf`: - -```nginx -server { - listen 80; - server_name git.yourdomain.com; - - location / { - proxy_pass http://nanaxe_gitea:3000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} - -server { - listen 80; - server_name api.yourdomain.com; - - location / { - proxy_pass http://nanaxe_api:8080; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} -``` - -## ✅ Проверка - -```bash -# Проверьте статус -docker compose ps - -# Проверьте API -curl http://api.yourdomain.com/health - -# Откройте Gitea -open http://git.yourdomain.com -``` - -## 📝 Примечания - -- Nanaxe использует ту же PostgreSQL базу, что и Consyscab -- Nginx вашего друга проксирует запросы к Nanaxe -- Все сервисы в одной Docker сети `consyscab-network` -- Данные Gitea хранятся в volume `gitea_data` -- Mindmaps хранятся в volume `mindmap_data`