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. -
GET /dao/{slug} -
PDP:
DAO_READ -
повертає
DaoRead+ агреговану інформацію (можна черезDaoOverview). -
PUT /dao/{slug} -
PDP:
DAO_MANAGE -
оновити
name/description/governance_model/.... -
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:
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:
@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