Clean up: move docs to Obsidian vault, remove outdated md files

This commit is contained in:
2026-04-21 15:54:20 +05:00
parent 448403d7ee
commit 4c50036172
22 changed files with 1295 additions and 2869 deletions
-50
View File
@@ -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
View File
@@ -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** 🚀
-246
View File
@@ -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 готов к работе! 🚀**
-194
View File
@@ -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 работает параллельно с вашим другим проектом!
-190
View File
@@ -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`** 🚀
-24
View File
@@ -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
View File
@@ -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 проверяют работоспособность
Ваш сервер всегда будет актуальным! 🚀
-162
View File
@@ -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
```
---
**Готово! 🚀**
-208
View File
@@ -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
View File
@@ -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** 🚀
-65
View File
@@ -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` - только просмотр
-224
View File
@@ -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
+321
View File
@@ -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));
}
}
+194
View File
@@ -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
}
}
}
+180
View File
@@ -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;
}
}
}
+12
View File
@@ -17,6 +17,8 @@ import '../screens/admin/projects_management_screen.dart';
import '../screens/admin/notifications_settings_screen.dart'; import '../screens/admin/notifications_settings_screen.dart';
import '../screens/admin/mind_map_admin_screen.dart'; import '../screens/admin/mind_map_admin_screen.dart';
import '../screens/admin/ai_stats_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'; import '../providers/auth_provider.dart';
// Глобальный ключ для навигации // Глобальный ключ для навигации
@@ -81,6 +83,16 @@ class AppRouter {
return MindMapSettingsScreen(projectId: projectId); 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( GoRoute(
+6
View File
@@ -4,6 +4,8 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'core/firebase_config.dart'; import 'core/firebase_config.dart';
import 'core/local_storage_service.dart'; import 'core/local_storage_service.dart';
import 'core/notification_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/theme.dart';
import 'core/router.dart'; import 'core/router.dart';
@@ -22,6 +24,10 @@ void main() async {
// Инициализация уведомлений // Инициализация уведомлений
await notificationService.initialize(); await notificationService.initialize();
// Инициализация Gitea синхронизации (mindmaps offline cache)
await GiteaSyncService().initialize();
await OfflineMindmapRepository().initialize();
runApp( runApp(
const ProviderScope( const ProviderScope(
child: NanaxeCrmApp(), child: NanaxeCrmApp(),
+228
View File
@@ -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);
});
+151
View File
@@ -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;
}
}
+203
View File
@@ -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,
),
),
],
),
),
),
);
}
}
-369
View File
@@ -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/)
-164
View File
@@ -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`