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,614 @@
# TASK_PHASE8_DAO_DASHBOARD.md
## PHASE 8 — DAO Dashboard (Governance + Treasury + Voting)
### Goal
Завершити **DAO-рівень governance** поверх вже готового microDAO Console:
- створити **dao-service** (бекенд) з повним CRUD;
- додати **governance models** (simple / quadratic / delegated);
- реалізувати **proposals + votes + treasury**;
- інтегрувати з **PDP/Auth** (Phase 4);
- зробити **DAO Dashboard UI** (frontend);
- підключити **NATS-події** для живого оновлення.
Фінальний результат:
DAO Dashboard, який показує стан DAO (учасники, казна, пропозиції, голосування) і дозволяє керувати governance.
---
## 0. Вихідні умови
Вважати, що в репозиторії вже є:
- **Phase 17 завершені** (Messenger, Agents, LLM, Security, Passkey, Agent Hub, microDAO Console);
- База даних (PostgreSQL) з таблицями `users`, `microdaos`, `microdao_members`, `microdao_treasury`, `microdao_settings`;
- **auth-service**, **pdp-service**, **usage-engine**, **messaging-service**, **agents-service**, **microdao-service**;
- Frontend (React/TS), з:
- `MicrodaoListPage.tsx`, `MicrodaoConsolePage.tsx`;
- auth/pdp інтеграцією;
- Agent Hub UI.
Цей таск додає **новий шар DAO** поверх існуючих microDAO.
---
## 1. Database: DAO Core Schema
Створити нову міграцію:
`migrations/009_create_dao_core.sql`
### 1.1. Таблиці
```sql
-- 1) DAO (верхній рівень governance)
create table if not exists dao (
id uuid primary key default gen_random_uuid(),
slug text not null unique,
name text not null,
description text,
microdao_id uuid not null references microdaos(id),
owner_user_id uuid not null references users(id),
governance_model text not null default 'simple', -- 'simple' | 'quadratic' | 'delegated'
voting_period_seconds integer not null default 604800, -- 7 днів
quorum_percent integer not null default 20, -- 20%
is_active boolean not null default true,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create index if not exists idx_dao_microdao_id on dao(microdao_id);
create index if not exists idx_dao_owner_user_id on dao(owner_user_id);
-- 2) DAO Members (над microdao_members)
create table if not exists dao_members (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
user_id uuid not null references users(id),
role text not null, -- 'owner' | 'admin' | 'member' | 'guest'
joined_at timestamptz not null default now()
);
create index if not exists idx_dao_members_user_id on dao_members(user_id);
create index if not exists idx_dao_members_dao_id_role on dao_members(dao_id, role);
-- 3) DAO Treasury (агрегований шар над microdao_treasury, але на рівні DAO)
create table if not exists dao_treasury (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
token_symbol text not null,
contract_address text,
balance numeric(30, 8) not null default 0,
updated_at timestamptz not null default now()
);
create unique index if not exists uq_dao_treasury_token
on dao_treasury(dao_id, token_symbol);
-- 4) DAO Proposals
create table if not exists dao_proposals (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
slug text not null,
title text not null,
description text,
created_by_user_id uuid not null references users(id),
created_at timestamptz not null default now(),
start_at timestamptz,
end_at timestamptz,
status text not null default 'draft', -- 'draft' | 'active' | 'passed' | 'rejected' | 'executed'
governance_model_override text, -- optional override
quorum_percent_override integer
);
create unique index if not exists uq_dao_proposals_slug
on dao_proposals(dao_id, slug);
-- 5) DAO Votes
create table if not exists dao_votes (
id uuid primary key default gen_random_uuid(),
proposal_id uuid not null references dao_proposals(id) on delete cascade,
voter_user_id uuid not null references users(id),
vote_value text not null, -- 'yes' | 'no' | 'abstain'
weight numeric(30, 8) not null, -- actual weight after applying model
raw_power numeric(30, 8), -- до обробки
created_at timestamptz not null default now()
);
create index if not exists idx_dao_votes_proposal_id on dao_votes(proposal_id);
create unique index if not exists uq_dao_votes_proposal_voter
on dao_votes(proposal_id, voter_user_id);
-- 6) DAO Roles (додатковий шар, якщо потрібні нестандартні ролі)
create table if not exists dao_roles (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
code text not null,
name text not null,
description text
);
create unique index if not exists uq_dao_roles_code
on dao_roles(dao_id, code);
-- 7) DAO Role Assignments
create table if not exists dao_role_assignments (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
user_id uuid not null references users(id),
role_code text not null,
assigned_at timestamptz not null default now()
);
create index if not exists idx_dao_role_assignments_user
on dao_role_assignments(user_id);
-- 8) DAO Audit Log
create table if not exists dao_audit_log (
id uuid primary key default gen_random_uuid(),
dao_id uuid not null references dao(id) on delete cascade,
actor_user_id uuid references users(id),
event_type text not null,
event_payload jsonb,
created_at timestamptz not null default now()
);
create index if not exists idx_dao_audit_log_dao_id
on dao_audit_log(dao_id);
```
Перевірити, що міграція застосовується без помилок.
---
## 2. Backend: `dao-service` (FastAPI)
Створити новий сервіс:
`services/dao-service/`:
* `main.py`
* `models.py`
* `repository_dao.py`
* `repository_proposals.py`
* `repository_votes.py`
* `governance_engine.py`
* `nats_events.py`
* `auth_client.py` / `pdp_client.py` (як thin-обгортки над існуючими)
* `requirements.txt`
* `Dockerfile`
* `README.md`
### 2.1. models.py
Оголосити Pydantic-схеми (адаптувати до стилю проєкту):
```python
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel
class DaoBase(BaseModel):
slug: str
name: str
description: str | None = None
class DaoCreate(DaoBase):
governance_model: str | None = None
voting_period_seconds: int | None = None
quorum_percent: int | None = None
class DaoUpdate(BaseModel):
name: str | None = None
description: str | None = None
governance_model: str | None = None
voting_period_seconds: int | None = None
quorum_percent: int | None = None
is_active: bool | None = None
class DaoRead(DaoBase):
id: str
microdao_id: str
owner_user_id: str
governance_model: str
voting_period_seconds: int
quorum_percent: int
is_active: bool
created_at: datetime
updated_at: datetime
class DaoMember(BaseModel):
id: str
user_id: str
role: str
joined_at: datetime
class DaoTreasuryItem(BaseModel):
token_symbol: str
contract_address: str | None = None
balance: Decimal
class ProposalBase(BaseModel):
slug: str
title: str
description: str | None = None
class ProposalCreate(ProposalBase):
start_at: datetime | None = None
end_at: datetime | None = None
class ProposalRead(ProposalBase):
id: str
dao_id: str
created_by_user_id: str
created_at: datetime
start_at: datetime | None
end_at: datetime | None
status: str
governance_model_override: str | None
quorum_percent_override: int | None
class VoteCreate(BaseModel):
vote_value: str # 'yes' | 'no' | 'abstain'
class VoteRead(BaseModel):
id: str
proposal_id: str
voter_user_id: str
vote_value: str
weight: Decimal
raw_power: Decimal | None
created_at: datetime
class DaoOverview(BaseModel):
dao: DaoRead
members_count: int
active_proposals_count: int
treasury_items: list[DaoTreasuryItem]
```
---
## 3. Repository layer
### 3.1. `repository_dao.py`
Реалізувати:
```python
async def list_dao_for_user(db, user_id: uuid.UUID) -> list[DaoRead]: ...
async def get_dao_by_slug(db, slug: str) -> DaoRead | None: ...
async def create_dao(db, *, microdao_id, owner_user_id, data: DaoCreate) -> DaoRead: ...
async def update_dao(db, *, dao_id, data: DaoUpdate) -> DaoRead | None: ...
async def soft_delete_dao(db, *, dao_id) -> None: ...
async def list_members(db, dao_id) -> list[DaoMember]: ...
async def add_member(db, dao_id, user_id, role) -> DaoMember: ...
async def remove_member(db, member_id) -> None: ...
async def get_treasury_items(db, dao_id) -> list[DaoTreasuryItem]: ...
```
### 3.2. `repository_proposals.py`
```python
async def list_proposals(db, dao_id: uuid.UUID) -> list[ProposalRead]: ...
async def get_proposal(db, proposal_id: uuid.UUID) -> ProposalRead | None: ...
async def get_proposal_by_slug(db, dao_id, slug) -> ProposalRead | None: ...
async def create_proposal(db, dao_id, created_by_user_id, data: ProposalCreate) -> ProposalRead: ...
async def update_proposal_status(db, proposal_id, status: str) -> ProposalRead | None: ...
```
### 3.3. `repository_votes.py`
```python
async def list_votes_for_proposal(db, proposal_id) -> list[VoteRead]: ...
async def create_or_update_vote(db, proposal_id, voter_user_id, vote_value, weight, raw_power) -> VoteRead: ...
```
---
## 4. Governance Engine
Файл: `services/dao-service/governance_engine.py`
Реалізувати три моделі:
```python
async def calculate_voting_power_simple(actor, dao: DaoRead) -> Decimal:
# 1 user = 1 голос
return Decimal(1)
async def calculate_voting_power_quadratic(actor, dao: DaoRead, base_power: Decimal) -> Decimal:
# weight = sqrt(base_power)
# base_power може бути, наприклад, кількістю токенів з treasury або окремої таблиці
from decimal import Decimal, getcontext
getcontext().prec = 28
return base_power.sqrt()
async def calculate_voting_power_delegated(actor, dao: DaoRead, delegation_graph) -> Decimal:
# з урахуванням делегацій
...
```
Також функцію **обчислення результату**:
```python
async def evaluate_proposal_outcome(
dao: DaoRead,
proposal: ProposalRead,
votes: list[VoteRead]
) -> dict:
# рахуємо total weight, yes/no/abstain, quorum, чи passed
...
```
---
## 5. NATS Events
Файл: `services/dao-service/nats_events.py`
Функція:
```python
async def publish_event(subject: str, payload: dict) -> None: ...
```
Викликати з backend:
* при створенні DAO:
`dao.event.created`
* при оновленні DAO:
`dao.event.updated`
* при оновленні treasury:
`dao.event.treasury_updated`
* при створенні пропозиції:
`dao.event.proposal_created`
* при активації/закритті пропозиції:
`dao.event.proposal_status_changed`
* при голосуванні:
`dao.event.vote_cast`
---
## 6. Routes (FastAPI)
У `services/dao-service/main.py` створити FastAPI app з router'ами:
### 6.1. Auth/PDP
Створити `auth_client.py`, `pdp_client.py` (як у інших сервісах):
```python
async def get_actor_identity(request) -> ActorIdentity: ...
async def check_permission(actor, action: str, resource: dict) -> None:
# кинути HTTPException(403) якщо deny
```
### 6.2. DAO Routes
Файл: `routes_dao.py`:
```python
router = APIRouter(prefix="/dao", tags=["dao"])
```
Endpoints:
1. `GET /dao`
* повертає DAO, де actor є членом:
* `list_dao_for_user(db, actor.user_id)`
2. `POST /dao`
* PDP: `DAO_CREATE`
* body: `DaoCreate`
* потрібно вказати `microdao_id` (як поле або через контекст)
* створюємо DAO, додаємо owner у `dao_members` з роллю `owner`.
3. `GET /dao/{slug}`
* PDP: `DAO_READ`
* повертає `DaoRead` + агреговану інформацію (можна через `DaoOverview`).
4. `PUT /dao/{slug}`
* PDP: `DAO_MANAGE`
* оновити `name/description/governance_model/...`.
5. `DELETE /dao/{slug}`
* PDP: `DAO_MANAGE`
* `is_active=false`.
### 6.3. Members Routes
`GET /dao/{slug}/members`
`POST /dao/{slug}/members`
`DELETE /dao/{slug}/members/{memberId}`
### 6.4. Treasury Routes
`GET /dao/{slug}/treasury`
`POST /dao/{slug}/treasury` (delta-операція: `token_symbol`, `delta`)
### 6.5. Proposals & Votes
`GET /dao/{slug}/proposals`
`POST /dao/{slug}/proposals`
`GET /dao/{slug}/proposals/{proposalSlug}`
`POST /dao/{slug}/proposals/{proposalSlug}/activate`
`POST /dao/{slug}/proposals/{proposalSlug}/close`
`GET /dao/{slug}/proposals/{proposalSlug}/votes`
`POST /dao/{slug}/proposals/{proposalSlug}/votes` (create/update vote)
---
## 7. Frontend: DAO Dashboard
У фронтенді створити структуру:
`src/api/dao.ts`
`src/features/dao/DaoListPage.tsx`
`src/features/dao/DaoDashboardPage.tsx`
`src/features/dao/components/...`
### 7.1. API Client
У `src/api/dao.ts`:
```ts
export async function getMyDaos(): Promise<DaoSummary[]> { ... }
export async function getDao(slug: string): Promise<DaoOverview> { ... }
export async function createDao(payload: DaoCreatePayload): Promise<DaoRead> { ... }
export async function getDaoMembers(slug: string): Promise<DaoMember[]> { ... }
export async function getDaoTreasury(slug: string): Promise<DaoTreasuryItem[]> { ... }
export async function getDaoProposals(slug: string): Promise<DaoProposal[]> { ... }
export async function createDaoProposal(slug: string, payload: ProposalCreatePayload): Promise<DaoProposal> { ... }
export async function getDaoProposal(slug: string, proposalSlug: string): Promise<DaoProposalDetail> { ... }
export async function castVote(slug: string, proposalSlug: string, payload: VotePayload): Promise<VoteRead> { ... }
```
Типи — у цьому ж файлі або в `types/dao.ts`.
### 7.2. Сторінки
#### `/dao`
`DaoListPage.tsx`:
* список DAO картками:
* назва
* опис
* governance модель
* кількість учасників, активних пропозицій
* кнопка "Створити DAO" (dialog → createDao)
#### `/dao/:slug`
`DaoDashboardPage.tsx`:
Tabs:
* Overview:
* загальна інформація
* короткі stats (members, proposals, treasury)
* Proposals:
* список пропозицій
* кнопка "Створити пропозицію"
* статуси, дедлайни
* Proposal detail:
* окрема панель (може бути drawer/side-panel або окрема сторінка)
* кнопка Vote (Yes/No/Abstain)
* показ quorum, результатів
* Treasury:
* список токенів, баланси
* простий графік
* Members:
* таблиця учасників
* роль
* Activity:
* стрічка подій DAO (з `dao_audit_log` або NATS)
---
## 8. WebSocket / Live Updates (MVP)
Опційно для цієї фази (якщо є час):
* у `dao-service`:
```python
@router.websocket("/ws/dao-events")
async def dao_events_ws(ws: WebSocket):
# підписка на NATS dao.event.* і пуш у клієнт
...
```
* на фронті `useDaoEvents(slug)`:
* фільтрувати тільки події конкретного DAO;
* оновлювати списки proposals/treasury.
Якщо часу мало — можна залишити це на наступну фазу, але місце під WS варто зарезервувати.
---
## 9. Інтеграція з microDAO Console
У `MicrodaoConsolePage.tsx` додати:
* секцію `Governance`:
* якщо для даного microDAO існує DAO → кнопка:
* "Відкрити DAO Dashboard"
* `navigate('/dao/' + daoSlug)`
* якщо не існує DAO → кнопка:
* "Створити DAO Governance"
* виклик `createDao({ microdaoId, slug, name, ... })`
(можна автогенерувати slug з microDAO slug).
---
## 10. Docker / Scripts
Оновити:
* `docker-compose.phase8.yml` (або доповнити існуючий compose):
* `dao-service` (порт, наприклад, 7016)
* `scripts/start-phase8.sh`:
* застосування `009` міграції;
* запуск `dao-service` разом з іншими сервісами.
---
## 11. Acceptance Criteria
Вважати Phase 8 виконаною, якщо:
* [ ] `009_create_dao_core.sql` застосовується без помилок;
* [ ] Запущено `dao-service` з `/health` endpoint;
* [ ] `GET /dao` повертає DAO, де actor є членом;
* [ ] `POST /dao` створює DAO, додає owner у members і публікує `dao.event.created`;
* [ ] `GET /dao/{slug}` повертає overview DAO (включно з members_count, active_proposals_count, treasury_items);
* [ ] `POST /dao/{slug}/proposals` створює пропозицію, `dao.event.proposal_created` публікується;
* [ ] `POST /dao/{slug}/proposals/{proposalSlug}/votes` створює/оновлює голос;
* [ ] у фронтенді `/dao` показує реальні DAO з БД;
* [ ] у фронтенді `/dao/:slug` показує Overview/Proposals/Treasury/Members з реальних endpoint'ів (без mock);
* [ ] PDP блокує доступ до DAO, де actor не є членом (403).
END OF TASK