feat: Add presence heartbeat for Matrix online status

- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
This commit is contained in:
Apple
2025-11-27 00:19:40 -08:00
parent 5bed515852
commit 3de3c8cb36
6371 changed files with 1317450 additions and 932 deletions

View File

@@ -0,0 +1,24 @@
FROM python:3.11-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application
COPY . .
# Expose port
EXPOSE 7002
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:7002/health')"
# Run application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7002"]

View File

@@ -0,0 +1,258 @@
# 🌌 DAARION Space Service
**Версія:** 1.0.0
**Статус:** Development (Mock Data)
**Порт:** 7002
---
## 📋 Опис
Space Service — це агрегатор даних для Space Dashboard в екосистемі DAARION. Відповідає за космічний шар візуалізації (planets, nodes, events).
### Функціонал
- 🪐 **Space Planets** — DAO-планети з метриками
- 🖥️ **Space Nodes** — детальні метрики нод
- 📡 **Space Events** — події космічного рівня
- 🔭 **Anomaly Detection** — виявлення аномалій
- 🌠 **Governance Temperature** — активність управління
---
## 🚀 Швидкий старт
### Через Docker Compose
```bash
# З кореня проєкту
./scripts/start-city-space-services.sh
```
### Локально (Development)
```bash
cd services/space-service
# Створити віртуальне середовище
python -m venv venv
source venv/bin/activate
# Встановити залежності
pip install -r requirements.txt
# Запустити сервіс
python main.py
```
---
## 📡 API Endpoints
### **GET** `/health`
Health check endpoint
---
### **GET** `/space/planets`
Повертає DAO-планети у космічному шарі
**Response:** `List[SpacePlanet]`
```json
[
{
"dao_id": "dao:3",
"name": "Aurora Circle",
"health": "good",
"treasury": 513200,
"activity": 0.84,
"governance_temperature": 72,
"anomaly_score": 0.04,
"position": { "x": 120, "y": 40, "z": -300 },
"node_count": 12,
"satellites": [...]
}
]
```
---
### **GET** `/space/nodes`
Повертає детальні метрики усіх нод
**Response:** `List[SpaceNode]`
```json
[
{
"node_id": "node:03",
"name": "Quantum Relay",
"microdao": "microdao:7",
"gpu": {
"load": 0.72,
"vram_used": 30.1,
"vram_total": 40.0,
"temperature": 71
},
"cpu": { "load": 0.44, "temperature": 62 },
"memory": { "used": 11.2, "total": 32.0 },
"network": {
"latency": 12,
"bandwidth_in": 540,
"bandwidth_out": 430,
"packet_loss": 0.01
},
"agents": 14,
"status": "healthy"
}
]
```
---
### **GET** `/space/events`
Поточні Space/DAO/Node події
**Query Parameters:**
- `seconds` (optional): Time window in seconds (default: 120)
**Response:** `List[SpaceEvent]`
```json
[
{
"type": "dao.vote.opened",
"dao_id": "dao:3",
"timestamp": 1735680041,
"severity": "info",
"meta": {
"proposal_id": "P-173",
"title": "Budget Allocation 2025"
}
}
]
```
---
## 🗺️ Схема агрегації даних
```
┌─────────────────────────────────────────────────────────┐
│ Space Service │
│ (Port: 7002) │
└─────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ microDAO │ │ Node │ │ Agent │
│ Service │ │ Metrics │ │ Registry │
└─────────────┘ └─────────────┘ └──────────────┘
┌──────────────┴──────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ NATS │ │ Redis / │
│ JetStream │ │ Timescale │
└──────────────┘ └──────────────┘
```
---
## 📊 Джерела даних
| Endpoint | Джерела | NATS Subject |
| --------------- | ------------------------------------- | --------------------- |
| `/planets` | microDAO service + Node metrics | `dao.state.*` |
| `/nodes` | NodeMetrics Agent → NATS | `node.metrics.*` |
| `/events` | JetStream Stream | `events.space.*` |
---
## 🔧 Конфігурація
### Environment Variables
```bash
# Service
LOG_LEVEL=INFO
ENVIRONMENT=development
# Redis
REDIS_URL=redis://redis:6379
# NATS
NATS_URL=nats://nats:4222
# CORS
CORS_ORIGINS=http://localhost:8899,https://daarion.city
```
---
## 🧪 Тестування
```bash
# Health check
curl http://localhost:7002/health
# Get planets
curl http://localhost:7002/space/planets
# Get nodes
curl http://localhost:7002/space/nodes
# Get events (last 60 seconds)
curl "http://localhost:7002/space/events?seconds=60"
# Через API Gateway
curl http://localhost:8080/space/planets
```
---
## 📚 Документація
- **OpenAPI Docs:** http://localhost:7002/docs
- **ReDoc:** http://localhost:7002/redoc
---
## 🗺️ Roadmap
### Phase 1: Mock Data ✅
- [x] FastAPI application
- [x] Mock planets/nodes/events
- [x] OpenAPI documentation
- [x] Docker setup
### Phase 2: Real Data Integration (Current)
- [ ] NATS client integration
- [ ] Redis client for metrics
- [ ] Real-time node metrics
- [ ] DAO state integration
- [ ] Event stream from JetStream
### Phase 3: Advanced Features
- [ ] Anomaly detection algorithm
- [ ] Governance temperature calculation
- [ ] Predictive analytics
- [ ] WebSocket support
---
## 📄 License
Proprietary — DAARION Ecosystem

View File

@@ -0,0 +1,239 @@
"""
DAARION Space Service
Агрегатор даних для Space Dashboard (planets, nodes, events)
"""
from fastapi import FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any
import logging
# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(
title="DAARION Space Service",
version="1.0.0",
description="Space data aggregator for DAARION cosmic layer"
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # TODO: обмежити в production
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ============================================================================
# Models
# ============================================================================
class Position3D(BaseModel):
x: float
y: float
z: float
class SpacePlanetSatellite(BaseModel):
node_id: str
gpu_load: float = Field(ge=0, le=1)
latency: float
agents: int
class SpacePlanet(BaseModel):
dao_id: str
name: str
health: str = Field(pattern="^(good|warn|critical)$")
treasury: float
activity: float = Field(ge=0, le=1)
governance_temperature: float
anomaly_score: float
position: Position3D
node_count: int
satellites: List[SpacePlanetSatellite]
class SpaceNodeGpu(BaseModel):
load: float = Field(ge=0, le=1)
vram_used: float
vram_total: float
temperature: float
class SpaceNodeCpu(BaseModel):
load: float = Field(ge=0, le=1)
temperature: float
class SpaceNodeMemory(BaseModel):
used: float
total: float
class SpaceNodeNetwork(BaseModel):
latency: float
bandwidth_in: float
bandwidth_out: float
packet_loss: float
class SpaceNode(BaseModel):
node_id: str
name: str
microdao: str
gpu: SpaceNodeGpu
cpu: SpaceNodeCpu
memory: SpaceNodeMemory
network: SpaceNodeNetwork
agents: int
status: str = Field(pattern="^(healthy|degraded|offline)$")
class SpaceEvent(BaseModel):
type: str
dao_id: Optional[str] = None
node_id: Optional[str] = None
timestamp: int
severity: str = Field(pattern="^(info|warn|error|critical)$")
meta: Dict[str, Any]
# ============================================================================
# Mock Data
# ============================================================================
MOCK_PLANETS = [
SpacePlanet(
dao_id="dao:3",
name="Aurora Circle",
health="good",
treasury=513200,
activity=0.84,
governance_temperature=72,
anomaly_score=0.04,
position=Position3D(x=120, y=40, z=-300),
node_count=12,
satellites=[
SpacePlanetSatellite(
node_id="node:03",
gpu_load=0.66,
latency=14,
agents=22
)
]
)
]
MOCK_NODES = [
SpaceNode(
node_id="node:03",
name="Quantum Relay",
microdao="microdao:7",
gpu=SpaceNodeGpu(load=0.72, vram_used=30.1, vram_total=40.0, temperature=71),
cpu=SpaceNodeCpu(load=0.44, temperature=62),
memory=SpaceNodeMemory(used=11.2, total=32.0),
network=SpaceNodeNetwork(latency=12, bandwidth_in=540, bandwidth_out=430, packet_loss=0.01),
agents=14,
status="healthy"
)
]
MOCK_EVENTS = [
SpaceEvent(
type="dao.vote.opened",
dao_id="dao:3",
timestamp=1735680041,
severity="info",
meta={"proposal_id": "P-173", "title": "Budget Allocation 2025"}
),
SpaceEvent(
type="node.alert.overload",
node_id="node:05",
timestamp=1735680024,
severity="warn",
meta={"gpu_load": 0.92}
)
]
# ============================================================================
# API Endpoints
# ============================================================================
@app.get("/health")
async def health():
"""Health check endpoint"""
return {"status": "healthy", "service": "space-service"}
@app.get("/space/planets", response_model=List[SpacePlanet])
async def get_planets():
"""
Повертає DAO-планети у космічному шарі DAARION
Джерела даних:
- DAO state → microDAO service (PostgreSQL)
- Node metrics → NATS node.metrics.*
- Space events → NATS JetStream events.space.*
"""
try:
logger.info("Fetching space planets")
# TODO: замінити на реальну агрегацію з microDAO service
return MOCK_PLANETS
except Exception as e:
logger.error(f"Error fetching planets: {e}")
raise HTTPException(status_code=500, detail="Failed to fetch planets")
@app.get("/space/nodes", response_model=List[SpaceNode])
async def get_nodes():
"""
Повертає усі ноди в космічній мережі DAARION
Джерела даних:
- Node metrics → NATS node.metrics.* → Redis/Timescale
- Agent counts → Router → Agent Registry
"""
try:
logger.info("Fetching space nodes")
# TODO: замінити на реальні метрики з NATS
return MOCK_NODES
except Exception as e:
logger.error(f"Error fetching nodes: {e}")
raise HTTPException(status_code=500, detail="Failed to fetch nodes")
@app.get("/space/events", response_model=List[SpaceEvent])
async def get_space_events(
seconds: int = Query(default=120, description="Time window in seconds")
):
"""
Поточні DAO/Node/Space події
Джерела даних:
- NATS JetStream events.space.*
- DAO events → dao.event.*
- Node alerts → node.alerts.*
"""
try:
logger.info(f"Fetching space events (last {seconds}s)")
# TODO: замінити на реальні події з JetStream
return MOCK_EVENTS
except Exception as e:
logger.error(f"Error fetching events: {e}")
raise HTTPException(status_code=500, detail="Failed to fetch events")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7002)

View File

@@ -0,0 +1,10 @@
fastapi==0.109.0
uvicorn[standard]==0.27.0
pydantic==2.5.3
python-dotenv==1.0.0
redis==5.0.1
asyncio-nats-client==0.11.5