- 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
18 KiB
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. Таблиці
-- 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.pymodels.pyrepository_dao.pyrepository_proposals.pyrepository_votes.pygovernance_engine.pynats_events.pyauth_client.py/pdp_client.py(як thin-обгортки над існуючими)requirements.txtDockerfileREADME.md
2.1. models.py
Оголосити Pydantic-схеми (адаптувати до стилю проєкту):
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
Реалізувати:
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
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
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
Реалізувати три моделі:
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:
# з урахуванням делегацій
...
Також функцію обчислення результату:
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
Функція:
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 (як у інших сервісах):
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:
router = APIRouter(prefix="/dao", tags=["dao"])
Endpoints:
-
GET /dao- повертає DAO, де actor є членом:
list_dao_for_user(db, actor.user_id)
-
POST /dao- PDP:
DAO_CREATE - body:
DaoCreate - потрібно вказати
microdao_id(як поле або через контекст) - створюємо DAO, додаємо owner у
dao_membersз роллюowner.
- PDP:
-
GET /dao/{slug}- PDP:
DAO_READ - повертає
DaoRead+ агреговану інформацію (можна черезDaoOverview).
- PDP:
-
PUT /dao/{slug}- PDP:
DAO_MANAGE - оновити
name/description/governance_model/....
- PDP:
-
DELETE /dao/{slug}- PDP:
DAO_MANAGE is_active=false.
- PDP:
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:
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)
- стрічка подій DAO (з
8. WebSocket / Live Updates (MVP)
Опційно для цієї фази (якщо є час):
- у
dao-service:
@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з/healthendpoint; 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