- 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
615 lines
18 KiB
Markdown
615 lines
18 KiB
Markdown
# 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 1–7 завершені** (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
|
||
|