feat: Add Nanaxe server with Docker, GitHub Actions CI/CD, and auto-deployment

- Add complete server infrastructure with Gitea, API, PostgreSQL
- Implement Obsidian-compatible markdown mindmap converter
- Add Flutter service for server synchronization
- Configure GitHub Actions for automated build and deploy
- Add production docker-compose with health checks
- Include VPS setup and deployment scripts
- Add comprehensive deployment documentation
This commit is contained in:
2026-04-19 22:18:25 +05:00
parent 52aea9ac87
commit f1e0f6b5ef
56 changed files with 8892 additions and 397 deletions
+50
View File
@@ -0,0 +1,50 @@
import 'package:cloud_firestore/cloud_firestore.dart';
/// Сервис для проверки прав доступа администраторов
class AdminAccessService {
// Список email'ов администраторов
static const List<String> adminEmails = [
'ormix16@gmail.com',
'almalexyz@gmail.com',
];
/// Проверка является ли пользователь администратором
static Future<bool> isAdmin(String userEmail) async {
return adminEmails.contains(userEmail.toLowerCase());
}
/// Получить список всех администраторов
static List<String> getAdminEmails() {
return List.from(adminEmails);
}
/// Добавить администратора (только для Firestore)
static Future<void> addAdmin(String email) async {
if (!adminEmails.contains(email.toLowerCase())) {
await FirebaseFirestore.instance
.collection('admin_access')
.doc(email.toLowerCase())
.set({
'email': email.toLowerCase(),
'addedAt': Timestamp.now(),
'role': 'admin',
});
}
}
/// Удалить администратора
static Future<void> removeAdmin(String email) async {
await FirebaseFirestore.instance
.collection('admin_access')
.doc(email.toLowerCase())
.delete();
}
/// Получить список администраторов из Firestore
static Future<List<String>> getAdminsFromFirestore() async {
final snapshot =
await FirebaseFirestore.instance.collection('admin_access').get();
return snapshot.docs.map((doc) => doc.id).toList();
}
}
+124 -7
View File
@@ -7,14 +7,95 @@ class DeepSeekService {
final String? apiKey;
final String baseUrl = 'https://api.deepseek.com';
// 🎯 РЕЖИМ MOCK - установите true для работы без API запросов
// Когда пополните баланс DeepSeek, установите false
final bool useMockMode = true;
DeepSeekService({this.apiKey});
// AI чат для mind maps
Future<Map<String, dynamic>> chatCompletion({
required List<Map<String, dynamic>> messages,
}) async {
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
return _mockChatResponse(messages);
}
try {
final response = await http.post(
Uri.parse('$baseUrl/v1/chat/completions'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $apiKey',
},
body: jsonEncode({
'model': 'deepseek-chat',
'messages': messages,
'temperature': 0.7,
'max_tokens': 2000,
}),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final content = data['choices'][0]['message']['content'];
// Пытаемся распарсить как JSON
final jsonMatch = RegExp(r'\{.*\}', dotAll: true).firstMatch(content);
if (jsonMatch != null) {
return jsonDecode(jsonMatch.group(0)!) as Map<String, dynamic>;
}
return {'text': content};
} else {
print('DeepSeek API Error: ${response.statusCode} - ${response.body}');
}
} catch (e) {
print('DeepSeek Service Error: $e');
}
return _mockChatResponse(messages);
}
// Mock ответ для чата
Map<String, dynamic> _mockChatResponse(List<Map<String, dynamic>> messages) {
final lastMessage = messages.lastWhere(
(m) => m['role'] == 'user',
orElse: () => {'content': ''},
);
final content = lastMessage['content'] as String;
// Простая логика для демонстрации
if (content.toLowerCase().contains('добавь') ||
content.toLowerCase().contains('add')) {
return {
'ACTION': 'add',
'TARGET': null,
'CHANGES': [
{
'action': 'add',
'title': 'AI Предложение: $content',
'level': 0,
'parentId': null
},
],
'EXPLANATION': 'Я добавил новый узел на основе вашего запроса',
};
}
return {
'text':
'Понял ваш запрос: "$content". В реальном режиме я предложу изменения для mind map.',
};
}
// Генерация подзадач из основной задачи
Future<List<Map<String, dynamic>>> generateSubtasks({
required String title,
required String description,
}) async {
if (apiKey == null || apiKey!.isEmpty) {
// Возвращаем mock данные пока нет баланса на DeepSeek
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
return _mockGenerateSubtasks(title, description);
}
@@ -56,9 +137,11 @@ class DeepSeekService {
}
} else {
print('DeepSeek API Error: ${response.statusCode} - ${response.body}');
print('⚠️ Используется mock режим как fallback');
}
} catch (e) {
print('DeepSeek Service Error: $e');
print('⚠️ Используется mock режим как fallback');
}
return _mockGenerateSubtasks(title, description);
@@ -70,7 +153,8 @@ class DeepSeekService {
required String description,
DateTime? dueDate,
}) async {
if (apiKey == null || apiKey!.isEmpty) {
// Возвращаем mock данные пока нет баланса на DeepSeek
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
return _mockPrioritizeTask(dueDate);
}
@@ -115,9 +199,12 @@ class DeepSeekService {
default:
return TaskPriority.medium;
}
} else {
print('⚠️ Используется mock режим как fallback');
}
} catch (e) {
print('DeepSeek Service Error: $e');
print('⚠️ Используется mock режим как fallback');
}
return _mockPrioritizeTask(dueDate);
@@ -128,8 +215,9 @@ class DeepSeekService {
required String title,
String? context,
}) async {
if (apiKey == null || apiKey!.isEmpty) {
return 'Описание для задачи: $title';
// Возвращаем mock данные пока нет баланса на DeepSeek
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
return _mockGenerateDescription(title, context);
}
try {
@@ -161,12 +249,15 @@ class DeepSeekService {
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return data['choices'][0]['message']['content'];
} else {
print('⚠️ Используется mock режим как fallback');
}
} catch (e) {
print('DeepSeek Service Error: $e');
print('⚠️ Используется mock режим как fallback');
}
return 'Описание для задачи: $title';
return _mockGenerateDescription(title, context);
}
// Умное планирование - распределение задач по времени
@@ -176,7 +267,8 @@ class DeepSeekService {
required DateTime endDate,
required int availableHoursPerDay,
}) async {
if (apiKey == null || apiKey!.isEmpty) {
// Возвращаем mock данные пока нет баланса на DeepSeek
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
return _mockSchedule(tasks, startDate, endDate, availableHoursPerDay);
}
@@ -238,9 +330,12 @@ Return JSON array: [{"taskId": index, "date": "YYYY-MM-DD", "hours": 0}]
};
}).toList();
}
} else {
print('⚠️ Используется mock режим как fallback');
}
} catch (e) {
print('DeepSeek Service Error: $e');
print('⚠️ Используется mock режим как fallback');
}
return _mockSchedule(tasks, startDate, endDate, availableHoursPerDay);
@@ -251,7 +346,8 @@ Return JSON array: [{"taskId": index, "date": "YYYY-MM-DD", "hours": 0}]
required String projectName,
required String description,
}) async {
if (apiKey == null || apiKey!.isEmpty) {
// Возвращаем mock данные пока нет баланса на DeepSeek
if (useMockMode || apiKey == null || apiKey!.isEmpty) {
return _mockMindMap(projectName);
}
@@ -297,9 +393,12 @@ Return JSON array of nodes with format:
final List<dynamic> nodes = jsonDecode(jsonMatch.group(0)!);
return nodes.cast<Map<String, dynamic>>();
}
} else {
print('⚠️ Используется mock режим как fallback');
}
} catch (e) {
print('DeepSeek Service Error: $e');
print('⚠️ Используется mock режим как fallback');
}
return _mockMindMap(projectName);
@@ -348,6 +447,24 @@ Return JSON array of nodes with format:
return TaskPriority.low;
}
String _mockGenerateDescription(String title, String? context) {
final descriptions = {
'Анализ требований':
'Изучить и проанализировать все требования проекта. Подготовить документацию с детальным описанием функционала и ожиданий пользователей.',
'Проектирование':
'Разработать архитектуру решения, создать схемы базы данных и определить основные компоненты системы.',
'Реализация':
'Написать чистый, поддерживаемый код согласно лучшим практикам. Реализовать весь запланированный функционал.',
'Тестирование':
'Провести комплексное тестирование: модульные, интеграционные и end-to-end тесты для обеспечения качества.',
'Деплой':
'Подготовить инфраструктуру, настроить CI/CD пайплайн и развернуть приложение на production сервере.',
};
return descriptions[title] ??
'Задача: $title. ${context ?? "Выполнить в срок с надлежащим качеством."}';
}
List<Map<String, dynamic>> _mockSchedule(
List<TaskModel> tasks,
DateTime startDate,
+64 -25
View File
@@ -1,4 +1,5 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../models/task_model.dart';
import '../models/alarm_model.dart';
import '../models/mind_map_model.dart';
@@ -29,20 +30,23 @@ class LocalStorageService {
// Tasks
Future<void> saveTask(TaskModel task) async {
await _tasksBox.put(task.id, task.toMap()); // Используем toMap() для Hive
final data = _convertTimestamps(task.toFirestore());
data['id'] = task.id; // Добавляем id для fromMap
await _tasksBox.put(task.id, data);
}
Future<void> saveTasks(List<TaskModel> tasks) async {
for (final task in tasks) {
await _tasksBox.put(task.id, task.toMap());
final data = _convertTimestamps(task.toFirestore());
data['id'] = task.id;
await _tasksBox.put(task.id, data);
}
}
List<TaskModel> getTasks() {
return _tasksBox.values.map((data) {
return TaskModel.fromFirestore(
_createMockDocument(data as Map<String, dynamic>),
);
final restoredData = _restoreTimestamps(data);
return TaskModel.fromMap(restoredData);
}).toList();
}
@@ -52,14 +56,14 @@ class LocalStorageService {
// Alarms
Future<void> saveAlarm(AlarmModel alarm) async {
await _alarmsBox.put(alarm.id, alarm.toFirestore());
await _alarmsBox.put(alarm.id, _convertTimestamps(alarm.toFirestore()));
}
List<AlarmModel> getAlarms() {
return _alarmsBox.values.map((data) {
return AlarmModel.fromFirestore(
_createMockDocument(data as Map<String, dynamic>),
);
return _alarmsBox.keys.map((id) {
final data = _alarmsBox.get(id) as Map<dynamic, dynamic>;
final restoredData = _restoreTimestamps(data);
return AlarmModel.fromMap(id.toString(), restoredData);
}).toList();
}
@@ -70,28 +74,28 @@ class LocalStorageService {
// Mind Maps
Future<void> saveMindMapNodes(List<MindMapNode> nodes) async {
for (final node in nodes) {
await _mindMapsBox.put(node.id, node.toFirestore());
await _mindMapsBox.put(node.id, _convertTimestamps(node.toFirestore()));
}
}
List<MindMapNode> getMindMapNodes() {
return _mindMapsBox.values.map((data) {
return MindMapNode.fromFirestore(
_createMockDocument(data as Map<String, dynamic>),
);
return _mindMapsBox.keys.map((id) {
final data = _mindMapsBox.get(id) as Map<dynamic, dynamic>;
final restoredData = _restoreTimestamps(data);
return MindMapNode.fromMap(id.toString(), restoredData);
}).toList();
}
// Events
Future<void> saveEvent(CalendarEventModel event) async {
await _eventsBox.put(event.id, event.toFirestore());
await _eventsBox.put(event.id, _convertTimestamps(event.toFirestore()));
}
List<CalendarEventModel> getEvents() {
return _eventsBox.values.map((data) {
return CalendarEventModel.fromFirestore(
_createMockDocument(data as Map<String, dynamic>),
);
return _eventsBox.keys.map((id) {
final data = _eventsBox.get(id) as Map<dynamic, dynamic>;
final restoredData = _restoreTimestamps(data);
return CalendarEventModel.fromMap(id.toString(), restoredData);
}).toList();
}
@@ -124,14 +128,49 @@ class LocalStorageService {
await _tasksBox.clear();
for (final task in merged.values) {
await _tasksBox.put(task.id, task.toFirestore());
await _tasksBox.put(task.id, _convertTimestamps(task.toFirestore()));
}
}
// Helper method to create mock DocumentSnapshot
dynamic _createMockDocument(Map<String, dynamic> data) {
// Простая эмуляция для конвертации
return data;
// Конвертирует Firestore Timestamp в ISO строки для Hive
Map<String, dynamic> _convertTimestamps(Map<String, dynamic> data) {
final converted = <String, dynamic>{};
for (final entry in data.entries) {
if (entry.value is Timestamp) {
// Конвертируем Timestamp → ISO строка
converted[entry.key] = {
'__type__': 'timestamp',
'value': (entry.value as Timestamp).toDate().toIso8601String(),
};
} else if (entry.value is Map<String, dynamic>) {
converted[entry.key] =
_convertTimestamps(entry.value as Map<String, dynamic>);
} else {
converted[entry.key] = entry.value;
}
}
return converted;
}
// Восстанавливает ISO строки обратно в Timestamp из Hive
Map<String, dynamic> _restoreTimestamps(Map<dynamic, dynamic> data) {
final converted = <String, dynamic>{};
for (final entry in data.entries) {
final key = entry.key.toString();
if (entry.value is Map &&
(entry.value as Map).containsKey('__type__') &&
(entry.value as Map)['__type__'] == 'timestamp') {
// Конвертируем ISO строку → Timestamp
final dateStr = (entry.value as Map)['value'] as String;
converted[key] = Timestamp.fromDate(DateTime.parse(dateStr));
} else if (entry.value is Map) {
converted[key] =
_restoreTimestamps(entry.value as Map<dynamic, dynamic>);
} else {
converted[key] = entry.value;
}
}
return converted;
}
}
+100
View File
@@ -0,0 +1,100 @@
import 'package:reactive_mind_map/reactive_mind_map.dart' as rm;
import '../models/mind_map_model.dart';
/// Адаптер для конвертации MindMapNode в формат reactive_mind_map
class MindMapAdapter {
/// Конвертирует список MindMapNode в rm.MindMapData для reactive_mind_map
static rm.MindMapData nodesToMindMapData(List<MindMapNode> nodes) {
if (nodes.isEmpty) {
return rm.MindMapData(
id: 'empty',
title: 'Пустая карта',
children: [],
);
}
// Находим корневой узел (level 0)
final rootNodes = nodes.where((n) => n.level == 0).toList();
if (rootNodes.isEmpty) {
// Если нет корневого узла, создаём его
return rm.MindMapData(
id: 'auto_root',
title: 'Автоматический корень',
children: _buildChildren(nodes, null, nodes),
);
}
final root = rootNodes.first;
return rm.MindMapData(
id: root.id,
title: root.title,
children: _buildChildren(nodes, root.id, nodes),
);
}
/// Рекурсивно строит дочерние узлы
static List<rm.MindMapData> _buildChildren(
List<MindMapNode> allNodes,
String? parentId,
List<MindMapNode> nodes,
) {
final children = allNodes.where((n) => n.parentId == parentId).toList();
return children.map((node) {
return rm.MindMapData(
id: node.id,
title: node.title,
children: _buildChildren(allNodes, node.id, nodes),
);
}).toList();
}
/// Конвертирует rm.MindMapData обратно в список MindMapNode
static List<MindMapNode> mindMapDataToNodes(
rm.MindMapData data,
String projectId, {
String? parentId,
int level = 0,
}) {
final nodes = <MindMapNode>[];
final now = DateTime.now();
nodes.add(MindMapNode(
id: data.id,
projectId: projectId,
parentId: parentId,
level: level,
title: data.title,
color: '#2196F3', // Default color, можно улучшить хранить цвет в metadata
createdAt: now,
updatedAt: now,
));
// Рекурсивно добавляем детей
for (final child in data.children!) {
nodes.addAll(mindMapDataToNodes(
child,
projectId,
parentId: data.id,
level: level + 1,
));
}
return nodes;
}
/// HEX → int (для reactive_mind_map)
static int _hexToColorInt(String hex) {
hex = hex.replaceAll('#', '');
if (hex.length == 6) {
return int.parse('FF$hex', radix: 16);
}
return 0xFF2196F3; // Default blue
}
/// int → HEX (для MindMapNode)
static String _colorIntToHex(int color) {
return '#${(color & 0xFFFFFF).toRadixString(16).padLeft(6, '0').toUpperCase()}';
}
}
+360
View File
@@ -0,0 +1,360 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
/// Сервис для синхронизации с Nanaxe Server
/// Поддерживает работу с mindmaps через REST API
class NanaxeServerService {
static final NanaxeServerService _instance = NanaxeServerService._internal();
factory NanaxeServerService() => _instance;
NanaxeServerService._internal();
String? _baseUrl;
String? _authToken;
static const String _prefsUrlKey = 'nanaxe_server_url';
static const String _prefsTokenKey = 'nanaxe_server_token';
/// Инициализация сервиса
Future<void> initialize() async {
final prefs = await SharedPreferences.getInstance();
_baseUrl = prefs.getString(_prefsUrlKey);
_authToken = prefs.getString(_prefsTokenKey);
}
/// Настройка сервера
Future<void> configure(String baseUrl, {String? token}) async {
_baseUrl = baseUrl.replaceAll(RegExp(r'/+$'), ''); // Remove trailing slashes
_authToken = token;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefsUrlKey, _baseUrl!);
if (token != null) {
await prefs.setString(_prefsTokenKey, token);
}
}
/// Очистка конфигурации
Future<void> clearConfiguration() async {
_baseUrl = null;
_authToken = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_prefsUrlKey);
await prefs.remove(_prefsTokenKey);
}
/// Проверка, настроен ли сервер
bool get isConfigured => _baseUrl != null;
/// Получение базового URL
String? get baseUrl => _baseUrl;
// ==================== AUTH ====================
/// Регистрация нового пользователя
Future<Map<String, dynamic>> register(String email, String password, String username) async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.post(
Uri.parse('$_baseUrl/api/auth/register'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': email,
'password': password,
'username': username,
}),
);
final data = jsonDecode(response.body);
if (response.statusCode == 201) {
_authToken = data['token'];
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefsTokenKey, _authToken!);
return data;
} else {
throw Exception(data['error'] ?? 'Registration failed');
}
}
/// Вход в систему
Future<Map<String, dynamic>> login(String email, String password) async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.post(
Uri.parse('$_baseUrl/api/auth/login'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'email': email,
'password': password,
}),
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
_authToken = data['token'];
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefsTokenKey, _authToken!);
return data;
} else {
throw Exception(data['error'] ?? 'Login failed');
}
}
/// Проверка валидности токена
Future<bool> validateToken() async {
if (_baseUrl == null || _authToken == null) return false;
try {
final response = await http.get(
Uri.parse('$_baseUrl/api/auth/validate'),
headers: _getHeaders(),
);
final data = jsonDecode(response.body);
return data['valid'] == true;
} catch (e) {
return false;
}
}
// ==================== MINDMAPS ====================
/// Получение списка всех mindmaps
Future<List<Map<String, dynamic>>> getMindMaps() async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.get(
Uri.parse('$_baseUrl/api/mindmaps'),
headers: _getHeaders(),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return List<Map<String, dynamic>>.from(data['mindmaps'] ?? []);
} else {
throw Exception('Failed to load mindmaps');
}
}
/// Получение одной mindmap
Future<Map<String, dynamic>> getMindMap(String id) async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.get(
Uri.parse('$_baseUrl/api/mindmaps/$id'),
headers: _getHeaders(),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to load mindmap');
}
}
/// Создание новой mindmap
Future<Map<String, dynamic>> createMindMap({
required String title,
String? description,
List<Map<String, dynamic>>? nodes,
Map<String, dynamic>? settings,
}) async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.post(
Uri.parse('$_baseUrl/api/mindmaps'),
headers: _getHeaders(),
body: jsonEncode({
'title': title,
'description': description ?? '',
'nodes': nodes ?? [],
'settings': settings ?? {},
}),
);
if (response.statusCode == 201) {
return jsonDecode(response.body);
} else {
final data = jsonDecode(response.body);
throw Exception(data['error'] ?? 'Failed to create mindmap');
}
}
/// Обновление mindmap
Future<Map<String, dynamic>> updateMindMap(
String id, {
String? title,
String? description,
List<Map<String, dynamic>>? nodes,
Map<String, dynamic>? settings,
}) async {
if (_baseUrl == null) throw Exception('Server not configured');
final body = <String, dynamic>{};
if (title != null) body['title'] = title;
if (description != null) body['description'] = description;
if (nodes != null) body['nodes'] = nodes;
if (settings != null) body['settings'] = settings;
final response = await http.put(
Uri.parse('$_baseUrl/api/mindmaps/$id'),
headers: _getHeaders(),
body: jsonEncode(body),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to update mindmap');
}
}
/// Удаление mindmap
Future<void> deleteMindMap(String id) async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.delete(
Uri.parse('$_baseUrl/api/mindmaps/$id'),
headers: _getHeaders(),
);
if (response.statusCode != 200) {
throw Exception('Failed to delete mindmap');
}
}
/// Получение mindmap как Markdown
Future<String> getMindMapAsMarkdown(String id) async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.get(
Uri.parse('$_baseUrl/api/mindmaps/$id/markdown'),
headers: _getHeaders(),
);
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Failed to load markdown');
}
}
/// Сохранение mindmap из Markdown
Future<Map<String, dynamic>> saveMindMapFromMarkdown(
String id,
String markdown,
) async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.post(
Uri.parse('$_baseUrl/api/mindmaps/$id/markdown'),
headers: _getHeaders(),
body: jsonEncode({'markdown': markdown}),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to save markdown');
}
}
// ==================== SYNC ====================
/// Получение статуса синхронизации
Future<Map<String, dynamic>> getSyncStatus() async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.get(
Uri.parse('$_baseUrl/api/sync/status'),
headers: _getHeaders(),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to get sync status');
}
}
/// Push изменений в Gitea
Future<Map<String, dynamic>> pushToGitea({String? commitMessage}) async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.post(
Uri.parse('$_baseUrl/api/sync/gitea/push'),
headers: _getHeaders(),
body: jsonEncode({
'commitMessage': commitMessage ?? 'Sync from Nanaxe CRM',
}),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to push to Gitea');
}
}
/// Pull изменений из Gitea
Future<Map<String, dynamic>> pullFromGitea() async {
if (_baseUrl == null) throw Exception('Server not configured');
final response = await http.post(
Uri.parse('$_baseUrl/api/sync/gitea/pull'),
headers: _getHeaders(),
body: jsonEncode({}),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to pull from Gitea');
}
}
// ==================== HELPERS ====================
Map<String, String> _getHeaders() {
final headers = {
'Content-Type': 'application/json',
};
if (_authToken != null) {
headers['Authorization'] = 'Bearer $_authToken';
}
return headers;
}
/// Проверка соединения с сервером
Future<bool> checkConnection() async {
if (_baseUrl == null) return false;
try {
final response = await http
.get(Uri.parse('$_baseUrl/health'))
.timeout(const Duration(seconds: 5));
return response.statusCode == 200;
} catch (e) {
return false;
}
}
}
/// Провайдер для работы с сервером
class ServerConfig {
final String baseUrl;
final String? token;
final bool isConnected;
ServerConfig({
required this.baseUrl,
this.token,
this.isConnected = false,
});
}
+30
View File
@@ -6,13 +6,17 @@ import '../screens/home_screen.dart';
import '../screens/tasks_screen.dart';
import '../screens/task_form_screen.dart';
import '../screens/mind_map_screen.dart';
import '../screens/mind_map_settings_screen.dart';
import '../screens/calendar_screen.dart';
import '../screens/alarms_screen.dart';
import '../screens/ai_test_screen.dart';
import '../screens/profile_screen.dart';
import '../screens/admin/admin_screen.dart';
import '../screens/admin/team_management_screen.dart';
import '../screens/admin/projects_management_screen.dart';
import '../screens/admin/notifications_settings_screen.dart';
import '../screens/admin/mind_map_admin_screen.dart';
import '../screens/admin/ai_stats_screen.dart';
import '../providers/auth_provider.dart';
// Глобальный ключ для навигации
@@ -67,6 +71,17 @@ class AppRouter {
path: 'mindmap',
name: 'mindmap',
builder: (context, state) => const MindMapScreen(),
routes: [
GoRoute(
path: 'settings',
name: 'mindmap-settings',
builder: (context, state) {
final projectId =
state.uri.queryParameters['projectId'] ?? 'default';
return MindMapSettingsScreen(projectId: projectId);
},
),
],
),
GoRoute(
path: 'calendar',
@@ -78,6 +93,11 @@ class AppRouter {
name: 'alarms',
builder: (context, state) => const AlarmsScreen(),
),
GoRoute(
path: 'profile',
name: 'profile',
builder: (context, state) => const ProfileScreen(),
),
GoRoute(
path: 'ai-test',
name: 'ai-test',
@@ -104,6 +124,16 @@ class AppRouter {
builder: (context, state) =>
const NotificationsSettingsScreen(),
),
GoRoute(
path: 'mindmaps',
name: 'admin-mindmaps',
builder: (context, state) => const MindMapAdminScreen(),
),
GoRoute(
path: 'ai-stats',
name: 'admin-ai-stats',
builder: (context, state) => const AIStatsScreen(),
),
],
),
],