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
+42 -26
View File
@@ -1,13 +1,15 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uuid/uuid.dart';
import 'package:reactive_mind_map/reactive_mind_map.dart' as rm;
import '../models/mind_map_model.dart';
import '../models/task_model.dart';
import '../core/firestore_service.dart';
import '../core/local_storage_service.dart';
import '../core/deepseek_service.dart';
import '../core/mind_map_adapter.dart';
// Provider для mind-карты проекта
final mindMapProvider = StateNotifierProvider.family<MindMapNotifier, AsyncValue<MindMapData>, String>((ref, projectId) {
final mindMapProvider = StateNotifierProvider.family<MindMapNotifier,
AsyncValue<MindMapData>, String>((ref, projectId) {
return MindMapNotifier(projectId);
});
@@ -31,9 +33,16 @@ class MindMapData {
// Получить родительский узел
MindMapNode? getParent(String nodeId) {
final node = nodes.firstWhere((n) => n.id == nodeId, orElse: () => throw Exception('Node not found'));
final node = nodes.firstWhere((n) => n.id == nodeId,
orElse: () => throw Exception('Node not found'));
if (node.parentId == null) return null;
return nodes.firstWhere((n) => n.id == node.parentId, orElse: () => throw Exception('Parent not found'));
return nodes.firstWhere((n) => n.id == node.parentId,
orElse: () => throw Exception('Parent not found'));
}
// Конвертировать в reactive_mind_map формат
rm.MindMapData toReactiveMindMapData() {
return MindMapAdapter.nodesToMindMapData(nodes);
}
}
@@ -49,8 +58,9 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
try {
// Загружаем из локального хранилища
final localNodes = localStorageService.getMindMapNodes();
final projectNodes = localNodes.where((n) => n.projectId == projectId).toList();
final projectNodes =
localNodes.where((n) => n.projectId == projectId).toList();
state = AsyncValue.data(_createMindMapData(projectNodes));
// TODO: Загрузить из Firestore и синхронизировать
@@ -90,7 +100,7 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
String icon = 'folder',
}) async {
try {
final parentNode = parentId != null
final parentNode = parentId != null
? state.value?.nodes.firstWhere((n) => n.id == parentId)
: null;
@@ -123,11 +133,11 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
final currentData = state.value;
if (currentData != null) {
final updatedNodes = [...currentData.nodes, node];
if (parentId != null) {
currentData.childrenMap.putIfAbsent(parentId, () => []).add(node.id);
}
state = AsyncValue.data(MindMapData(
nodes: updatedNodes,
childrenMap: currentData.childrenMap,
@@ -143,7 +153,8 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
}
// Обновление узла
Future<void> updateNode(String nodeId, {
Future<void> updateNode(
String nodeId, {
String? title,
String? color,
String? icon,
@@ -203,7 +214,9 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
_collectChildNodes(nodeId, currentData, nodesToDelete);
// Сохраняем для удаления
final nodes = currentData.nodes.where((n) => !nodesToDelete.contains(n.id)).toList();
final nodes = currentData.nodes
.where((n) => !nodesToDelete.contains(n.id))
.toList();
// Сохраняем локально (удаляем)
for (final id in nodesToDelete) {
@@ -234,7 +247,8 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
}
// Рекурсивный сбор дочерних узлов
void _collectChildNodes(String parentId, MindMapData data, Set<String> result) {
void _collectChildNodes(
String parentId, MindMapData data, Set<String> result) {
final childIds = data.childrenMap[parentId] ?? [];
for (final childId in childIds) {
result.add(childId);
@@ -260,7 +274,7 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
final nodeData = nodesData[i];
final level = nodeData['level'] as int;
final parentId = nodeData['parentId'] as int?;
String? actualParentId;
if (parentId != null && parentId < nodes.length) {
actualParentId = nodes[parentId].id;
@@ -272,16 +286,16 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
x: (level * 200).toDouble(), // Автоматическое позиционирование
y: (i * 50).toDouble(),
);
nodes.add(node);
if (level == 0) {
rootNode = node;
}
}
return nodes;
} catch (e, st) {
} catch (e) {
rethrow;
}
}
@@ -300,16 +314,18 @@ class MindMapNotifier extends StateNotifier<AsyncValue<MindMapData>> {
return {
'projectId': projectId,
'rootNodeId': data.rootNodeId,
'nodes': data.nodes.map((n) => {
'id': n.id,
'parentId': n.parentId,
'level': n.level,
'title': n.title,
'x': n.x,
'y': n.y,
'color': n.color,
'icon': n.icon,
}).toList(),
'nodes': data.nodes
.map((n) => {
'id': n.id,
'parentId': n.parentId,
'level': n.level,
'title': n.title,
'x': n.x,
'y': n.y,
'color': n.color,
'icon': n.icon,
})
.toList(),
};
}