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,289 @@
-- ============================================================================
-- Migration 009: DAO Core Tables
-- Phase 8: DAO Dashboard (Governance + Treasury + Voting)
-- ============================================================================
-- ============================================================================
-- Table: dao
-- Purpose: DAO entities with governance configuration
-- ============================================================================
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) ON DELETE CASCADE,
owner_user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
governance_model TEXT NOT NULL DEFAULT 'simple', -- 'simple' | 'quadratic' | 'delegated'
voting_period_seconds INTEGER NOT NULL DEFAULT 604800, -- 7 days
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 idx_dao_microdao_id ON dao(microdao_id);
CREATE INDEX idx_dao_owner_user_id ON dao(owner_user_id);
CREATE INDEX idx_dao_is_active ON dao(is_active);
COMMENT ON TABLE dao IS 'DAO entities with governance configuration';
COMMENT ON COLUMN dao.governance_model IS 'simple, quadratic, or delegated voting';
COMMENT ON COLUMN dao.voting_period_seconds IS 'Default voting period for proposals';
COMMENT ON COLUMN dao.quorum_percent IS 'Minimum participation percentage for valid vote';
-- ============================================================================
-- Table: dao_members
-- Purpose: DAO membership with roles
-- ============================================================================
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) ON DELETE CASCADE,
role TEXT NOT NULL, -- 'owner' | 'admin' | 'member' | 'guest'
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(dao_id, user_id)
);
CREATE INDEX idx_dao_members_user_id ON dao_members(user_id);
CREATE INDEX idx_dao_members_dao_id ON dao_members(dao_id);
CREATE INDEX idx_dao_members_dao_id_role ON dao_members(dao_id, role);
COMMENT ON TABLE dao_members IS 'DAO membership with roles';
COMMENT ON COLUMN dao_members.role IS 'owner, admin, member, guest';
-- ============================================================================
-- Table: dao_treasury
-- Purpose: Token balances for DAO treasury
-- ============================================================================
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(),
UNIQUE(dao_id, token_symbol)
);
CREATE INDEX idx_dao_treasury_dao_id ON dao_treasury(dao_id);
COMMENT ON TABLE dao_treasury IS 'Token balances for DAO treasury';
COMMENT ON COLUMN dao_treasury.balance IS 'Token balance with 8 decimal precision';
COMMENT ON COLUMN dao_treasury.contract_address IS 'Optional smart contract address';
-- ============================================================================
-- Table: dao_proposals
-- Purpose: Governance proposals for voting
-- ============================================================================
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) ON DELETE RESTRICT,
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,
quorum_percent_override INTEGER,
UNIQUE(dao_id, slug)
);
CREATE INDEX idx_dao_proposals_dao_id ON dao_proposals(dao_id);
CREATE INDEX idx_dao_proposals_status ON dao_proposals(status);
CREATE INDEX idx_dao_proposals_created_by ON dao_proposals(created_by_user_id);
COMMENT ON TABLE dao_proposals IS 'Governance proposals for voting';
COMMENT ON COLUMN dao_proposals.status IS 'draft, active, passed, rejected, executed';
COMMENT ON COLUMN dao_proposals.governance_model_override IS 'Override DAO default governance model';
-- ============================================================================
-- Table: dao_votes
-- Purpose: Individual votes on proposals
-- ============================================================================
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) ON DELETE CASCADE,
vote_value TEXT NOT NULL, -- 'yes' | 'no' | 'abstain'
weight NUMERIC(30, 8) NOT NULL, -- actual weight after applying governance model
raw_power NUMERIC(30, 8), -- raw voting power before governance model
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(proposal_id, voter_user_id)
);
CREATE INDEX idx_dao_votes_proposal_id ON dao_votes(proposal_id);
CREATE INDEX idx_dao_votes_voter_user_id ON dao_votes(voter_user_id);
COMMENT ON TABLE dao_votes IS 'Individual votes on proposals';
COMMENT ON COLUMN dao_votes.vote_value IS 'yes, no, abstain';
COMMENT ON COLUMN dao_votes.weight IS 'Calculated weight after governance model';
COMMENT ON COLUMN dao_votes.raw_power IS 'Raw voting power before calculation';
-- ============================================================================
-- Table: dao_roles
-- Purpose: Custom roles for DAO
-- ============================================================================
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,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(dao_id, code)
);
CREATE INDEX idx_dao_roles_dao_id ON dao_roles(dao_id);
COMMENT ON TABLE dao_roles IS 'Custom roles for DAO';
COMMENT ON COLUMN dao_roles.code IS 'Unique role code (e.g., treasury_manager)';
-- ============================================================================
-- Table: dao_role_assignments
-- Purpose: Custom role assignments to users
-- ============================================================================
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) ON DELETE CASCADE,
role_code TEXT NOT NULL,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(dao_id, user_id, role_code)
);
CREATE INDEX idx_dao_role_assignments_user_id ON dao_role_assignments(user_id);
CREATE INDEX idx_dao_role_assignments_dao_id ON dao_role_assignments(dao_id);
COMMENT ON TABLE dao_role_assignments IS 'Custom role assignments to users';
-- ============================================================================
-- Table: dao_audit_log
-- Purpose: Audit log for all DAO actions
-- ============================================================================
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) ON DELETE SET NULL,
event_type TEXT NOT NULL,
event_payload JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_dao_audit_log_dao_id ON dao_audit_log(dao_id);
CREATE INDEX idx_dao_audit_log_created_at ON dao_audit_log(created_at DESC);
CREATE INDEX idx_dao_audit_log_event_type ON dao_audit_log(event_type);
COMMENT ON TABLE dao_audit_log IS 'Audit log for all DAO actions';
COMMENT ON COLUMN dao_audit_log.event_type IS 'Type of event (created, updated, voted, etc.)';
-- ============================================================================
-- Update Trigger: dao.updated_at
-- ============================================================================
CREATE OR REPLACE FUNCTION update_dao_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_update_dao_updated_at
BEFORE UPDATE ON dao
FOR EACH ROW
EXECUTE FUNCTION update_dao_updated_at();
-- ============================================================================
-- Seed Data: Sample DAO for DAARION microDAO
-- ============================================================================
-- Create DAO for DAARION microDAO
INSERT INTO dao (slug, name, description, microdao_id, owner_user_id, governance_model, quorum_percent)
SELECT
'daarion-governance',
'DAARION Governance',
'Децентралізоване управління екосистемою DAARION',
m.id,
m.owner_user_id,
'simple',
20
FROM microdaos m
WHERE m.external_id = 'microdao:daarion'
LIMIT 1
ON CONFLICT (slug) DO NOTHING;
-- Add owner as DAO member
INSERT INTO dao_members (dao_id, user_id, role)
SELECT
d.id,
d.owner_user_id,
'owner'
FROM dao d
WHERE d.slug = 'daarion-governance'
ON CONFLICT (dao_id, user_id) DO NOTHING;
-- Initialize DAO treasury with DAARION token
INSERT INTO dao_treasury (dao_id, token_symbol, balance)
SELECT
d.id,
'DAARION',
1000000.0
FROM dao d
WHERE d.slug = 'daarion-governance'
ON CONFLICT (dao_id, token_symbol) DO NOTHING;
-- Create sample proposal
INSERT INTO dao_proposals (dao_id, slug, title, description, created_by_user_id, status, start_at, end_at)
SELECT
d.id,
'proposal-1-funding',
'Фінансування розвитку Agent Hub',
'Пропозиція виділити 10,000 DAARION токенів на розвиток Agent Hub у Q1 2025',
d.owner_user_id,
'active',
NOW(),
NOW() + INTERVAL '7 days'
FROM dao d
WHERE d.slug = 'daarion-governance'
ON CONFLICT (dao_id, slug) DO NOTHING;
-- ============================================================================
-- Grants
-- ============================================================================
GRANT SELECT, INSERT, UPDATE, DELETE ON dao TO postgres;
GRANT SELECT, INSERT, UPDATE, DELETE ON dao_members TO postgres;
GRANT SELECT, INSERT, UPDATE, DELETE ON dao_treasury TO postgres;
GRANT SELECT, INSERT, UPDATE, DELETE ON dao_proposals TO postgres;
GRANT SELECT, INSERT, UPDATE, DELETE ON dao_votes TO postgres;
GRANT SELECT, INSERT, UPDATE, DELETE ON dao_roles TO postgres;
GRANT SELECT, INSERT, UPDATE, DELETE ON dao_role_assignments TO postgres;
GRANT SELECT, INSERT, UPDATE, DELETE ON dao_audit_log TO postgres;
-- ============================================================================
-- Migration Complete
-- ============================================================================
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name IN ('dao', 'dao_members', 'dao_treasury', 'dao_proposals', 'dao_votes')
) THEN
RAISE NOTICE 'Migration 009: DAO Core Tables created successfully';
ELSE
RAISE EXCEPTION 'Migration 009: Failed to create tables';
END IF;
END $$;