Clean up: move docs to Obsidian vault, remove outdated md files
This commit is contained in:
-50
@@ -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
|
||||
|
||||
Если работает — всё настроено правильно! 🎉
|
||||
-284
@@ -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<String> tags;
|
||||
DateTime? dueDate;
|
||||
int estimatedHours;
|
||||
Map<String, dynamic> 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** 🚀
|
||||
@@ -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<void> 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 готов к работе! 🚀**
|
||||
@@ -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 работает параллельно с вашим другим проектом!
|
||||
@@ -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`** 🚀
|
||||
@@ -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)
|
||||
-349
@@ -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 проверяют работоспособность
|
||||
|
||||
Ваш сервер всегда будет актуальным! 🚀
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Готово! 🚀**
|
||||
@@ -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
|
||||
-340
@@ -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** 🚀
|
||||
@@ -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` - только просмотр
|
||||
@@ -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
|
||||
@@ -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<String, dynamic> 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<void> initialize() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_baseUrl = prefs.getString(_prefsBaseUrlKey);
|
||||
_token = prefs.getString(_prefsTokenKey);
|
||||
_owner = prefs.getString(_prefsOwnerKey);
|
||||
_repo = prefs.getString(_prefsRepoKey);
|
||||
}
|
||||
|
||||
/// Настройка подключения
|
||||
Future<void> 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<bool> testConnection() async {
|
||||
if (!isConfigured) return false;
|
||||
try {
|
||||
final response = await _get('/api/v1/user');
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Получить информацию о текущем пользователе
|
||||
Future<Map<String, dynamic>?> getCurrentUser() async {
|
||||
final response = await _get('/api/v1/user');
|
||||
if (response.statusCode == 200) {
|
||||
return jsonDecode(response.body) as Map<String, dynamic>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==================== REPOSITORY CONTENTS ====================
|
||||
|
||||
/// Получить список файлов в директории репозитория
|
||||
Future<List<GiteaFile>> 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<dynamic> data = jsonDecode(response.body);
|
||||
return data
|
||||
.map((e) => GiteaFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Получить содержимое одного файла
|
||||
Future<GiteaFile> 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<String, dynamic>;
|
||||
return GiteaFile.fromJson(data);
|
||||
}
|
||||
|
||||
/// Обновить/создать файл в репозитории
|
||||
Future<void> 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 = <String, dynamic>{
|
||||
'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<void> 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<List<GiteaFile>> 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<String, dynamic>;
|
||||
final List<dynamic> 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<Map<String, dynamic>?> 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<dynamic> data = jsonDecode(response.body);
|
||||
if (data.isNotEmpty) return data.first as Map<String, dynamic>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Получить историю коммитов
|
||||
Future<List<Map<String, dynamic>>> 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<dynamic> data = jsonDecode(response.body);
|
||||
return data.cast<Map<String, dynamic>>();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ==================== HELPERS ====================
|
||||
|
||||
Map<String, String> _getHeaders() {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'token $_token',
|
||||
};
|
||||
}
|
||||
|
||||
Future<http.Response> _get(String path) async {
|
||||
final uri = Uri.parse('$_baseUrl$path');
|
||||
return http.get(uri, headers: _getHeaders());
|
||||
}
|
||||
|
||||
Future<http.Response> _put(String path,
|
||||
{required Map<String, dynamic> body}) async {
|
||||
final uri = Uri.parse('$_baseUrl$path');
|
||||
return http.put(
|
||||
uri,
|
||||
headers: _getHeaders(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
}
|
||||
|
||||
Future<http.Response> _delete(String path,
|
||||
{required Map<String, dynamic> 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));
|
||||
}
|
||||
}
|
||||
@@ -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<MindMapNode> parse(String markdown, String projectId) {
|
||||
final lines = markdown.split('\n');
|
||||
final nodes = <MindMapNode>[];
|
||||
final now = DateTime.now();
|
||||
|
||||
String? currentParentId;
|
||||
final Map<int, String> 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<MindMapNode> 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<MindMapNode>.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<String> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, dynamic> toJson() => {
|
||||
'path': path,
|
||||
'content': content,
|
||||
'sha': sha,
|
||||
'lastSync': lastSync.toIso8601String(),
|
||||
'lastModified': lastModified.toIso8601String(),
|
||||
'branch': branch,
|
||||
};
|
||||
|
||||
factory CachedMindmap.fromJson(Map<String, dynamic> 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<String>? _box;
|
||||
Box<String>? _queueBox;
|
||||
|
||||
Future<void> initialize() async {
|
||||
_box = await Hive.openBox<String>(_boxName);
|
||||
_queueBox = await Hive.openBox<String>(_queueBoxName);
|
||||
}
|
||||
|
||||
bool get isInitialized => _box != null && _box!.isOpen;
|
||||
|
||||
// ==================== CACHE ====================
|
||||
|
||||
/// Сохранить файл в кэш
|
||||
Future<void> 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<CachedMindmap> getAllMindmaps() {
|
||||
if (!isInitialized) return [];
|
||||
return _box!.values
|
||||
.map((raw) {
|
||||
try {
|
||||
return CachedMindmap.fromJson(jsonDecode(raw));
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<CachedMindmap>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Удалить файл из кэша
|
||||
Future<void> deleteMindmap(String path) async {
|
||||
if (!isInitialized) return;
|
||||
await _box!.delete(path);
|
||||
}
|
||||
|
||||
/// Очистить весь кэш
|
||||
Future<void> clearCache() async {
|
||||
if (!isInitialized) return;
|
||||
await _box!.clear();
|
||||
}
|
||||
|
||||
// ==================== SYNC QUEUE ====================
|
||||
|
||||
/// Добавить изменение в очередь (для offline → online sync)
|
||||
Future<void> 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<Map<String, dynamic>> getPendingChanges() {
|
||||
if (!isInitialized) return [];
|
||||
return _queueBox!.values
|
||||
.map((raw) {
|
||||
try {
|
||||
return jsonDecode(raw) as Map<String, dynamic>;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.toList()
|
||||
..sort((a, b) => (a['timestamp'] ?? '').compareTo(b['timestamp'] ?? ''));
|
||||
}
|
||||
|
||||
/// Удалить выполненное изменение из очереди
|
||||
Future<void> removeQueuedChange(String key) async {
|
||||
if (!isInitialized) return;
|
||||
await _queueBox!.delete(key);
|
||||
}
|
||||
|
||||
/// Очистить очередь
|
||||
Future<void> clearQueue() async {
|
||||
if (!isInitialized) return;
|
||||
await _queueBox!.clear();
|
||||
}
|
||||
|
||||
bool get hasPendingChanges => (_queueBox?.length ?? 0) > 0;
|
||||
|
||||
// ==================== SYNC STATE ====================
|
||||
|
||||
/// Сохранить время последней успешной синхронизации
|
||||
Future<void> 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<String, dynamic>;
|
||||
return DateTime.tryParse(data['lastSync'] ?? '');
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<GiteaSyncService>((ref) {
|
||||
return GiteaSyncService();
|
||||
});
|
||||
|
||||
/// Provider для OfflineMindmapRepository
|
||||
final offlineMindmapRepoProvider = Provider<OfflineMindmapRepository>((ref) {
|
||||
return OfflineMindmapRepository();
|
||||
});
|
||||
|
||||
/// State для mindmap синхронизации
|
||||
class MindmapSyncState {
|
||||
final List<GiteaFile> files;
|
||||
final List<CachedMindmap> 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<GiteaFile>? files,
|
||||
List<CachedMindmap>? 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<MindmapSyncState> {
|
||||
final GiteaSyncService _gitea;
|
||||
final OfflineMindmapRepository _repo;
|
||||
|
||||
MindmapSyncNotifier(this._gitea, this._repo) : super(MindmapSyncState());
|
||||
|
||||
/// Инициализация сервиса
|
||||
Future<void> initialize() async {
|
||||
await _repo.initialize();
|
||||
await _gitea.initialize();
|
||||
state = state.copyWith(isConfigured: _gitea.isConfigured);
|
||||
|
||||
if (state.isConfigured) {
|
||||
await loadCachedMindmaps();
|
||||
}
|
||||
}
|
||||
|
||||
/// Настройка Gitea
|
||||
Future<void> 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<void> loadCachedMindmaps() async {
|
||||
final cached = _repo.getAllMindmaps();
|
||||
state = state.copyWith(cachedMindmaps: cached);
|
||||
}
|
||||
|
||||
/// Полная синхронизация с Gitea (download)
|
||||
Future<void> 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<MindMapNode>? parseMindmap(String path) {
|
||||
final cached = _repo.getMindmap(path);
|
||||
if (cached == null) return null;
|
||||
|
||||
return MarkdownMindmapParser.parse(
|
||||
cached.content,
|
||||
path.replaceAll('/', '_'),
|
||||
);
|
||||
}
|
||||
|
||||
/// Сохранить изменения mindmap и запушить в Gitea
|
||||
Future<void> saveAndPush({
|
||||
required String path,
|
||||
required List<MindMapNode> 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<void> 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<MindmapSyncNotifier, MindmapSyncState>((ref) {
|
||||
final gitea = ref.watch(giteaSyncServiceProvider);
|
||||
final repo = ref.watch(offlineMindmapRepoProvider);
|
||||
return MindmapSyncNotifier(gitea, repo);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<GiteaSyncSettingsScreen> createState() => _GiteaSyncSettingsScreenState();
|
||||
}
|
||||
|
||||
class _GiteaSyncSettingsScreenState extends ConsumerState<GiteaSyncSettingsScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
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<void> _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<void> _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 <your-repo-url> 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/)
|
||||
@@ -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`
|
||||
Reference in New Issue
Block a user