Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
22be3f3
Add contract event type definitions
Mosas2000 Apr 21, 2026
e56157e
Add contract event normalization and API fetching
Mosas2000 Apr 21, 2026
e8b6f96
Add unit tests for contract event normalization
Mosas2000 Apr 21, 2026
b6cbb04
Add ContractEventStream React component with filtering
Mosas2000 Apr 21, 2026
3e71302
Export ContractEventStream from dashboard index
Mosas2000 Apr 21, 2026
9536b1b
Integrate ContractEventStream into GovernanceAnalyticsDashboard
Mosas2000 Apr 21, 2026
0ac8686
Add notification preference types
Mosas2000 Apr 21, 2026
9c908e6
Add governance notification preferences management
Mosas2000 Apr 21, 2026
13d68a3
Add governance notification creation and deduplication logic
Mosas2000 Apr 21, 2026
7d46eb8
Add tests for governance notification creation
Mosas2000 Apr 21, 2026
8d45069
Add notification preferences UI modal
Mosas2000 Apr 21, 2026
d244d8d
Add notification toast display component
Mosas2000 Apr 21, 2026
c80faad
Add notification management hook
Mosas2000 Apr 21, 2026
0e65128
Add governance notification manager component
Mosas2000 Apr 21, 2026
ad6bd35
Integrate notification manager into root layout
Mosas2000 Apr 21, 2026
86c8b72
Add hook for monitoring contract events with callbacks
Mosas2000 Apr 21, 2026
48125e8
Add hook for automatic governance event notifications
Mosas2000 Apr 21, 2026
b0caa68
Add tests for contract events hook
Mosas2000 Apr 21, 2026
8d45bdc
Add notification list display component
Mosas2000 Apr 21, 2026
96939f6
Add notification center dropdown component
Mosas2000 Apr 21, 2026
6c12d3f
Add tests for notification preferences storage
Mosas2000 Apr 21, 2026
902ed9e
Add governance features documentation
Mosas2000 Apr 21, 2026
40b34ff
Add governance configuration and utility functions
Mosas2000 Apr 21, 2026
b800653
Add notification utility functions
Mosas2000 Apr 21, 2026
015e340
Add tests for notification utilities
Mosas2000 Apr 21, 2026
e62001c
Add event analysis and utility functions
Mosas2000 Apr 21, 2026
28db5c0
Add tests for event analysis utilities
Mosas2000 Apr 21, 2026
36122cf
Add event analytics panel component
Mosas2000 Apr 21, 2026
31af1b4
Add implementation checklist and feature summary
Mosas2000 Apr 21, 2026
a61a001
Add tests for implementation checklist
Mosas2000 Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 275 additions & 0 deletions GOVERNANCE_FEATURES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
# Governance Features Documentation

## Issue #265: Live Contract Event Stream

### Overview
A real-time feed of contract and governance events that allows users to track DAO activity as it happens.

### Components

#### ContractEventStream.tsx
Main UI component displaying a filterable event feed.

**Props:**
- `contractPrincipal: string` - The contract to monitor
- `apiUrl?: string` - Hiro API endpoint (default: mainnet)
- `pollInterval?: number` - Polling interval in ms (default: 20000)

**Features:**
- Real-time event polling
- Filterable by event category (stake, proposal, vote, cancel, execute, treasury)
- Show/hide failed transactions
- Manual refresh button
- Explorer links for each event

#### contract-events.ts
Core logic for fetching and normalizing contract events.

**Functions:**
- `fetchContractEventStream()` - Fetches events from Hiro API
- `normalizeContractTransaction()` - Converts raw transactions to readable events

**Event Categories:**
- `stake` - Staking transactions
- `proposal` - New proposals
- `vote` - Voting transactions
- `cancel` - Cancelled proposals
- `execute` - Executed proposals
- `treasury` - Treasury transfers

### Usage

```tsx
import { ContractEventStream } from '@/components/dashboard/ContractEventStream';

export default function Page() {
return (
<ContractEventStream
contractPrincipal="SP2ZNGJ85ENDY6QTHQ0YCWM1GRFX77YXF1W8F25J9.sprint-fund"
pollInterval={20000}
/>
);
}
```

---

## Issue #259: Governance Notifications

### Overview
Automated notifications for important governance events, helping users stay informed without constant app refreshing.

### Components

#### GovernanceNotificationManager.tsx
Manages the notification system lifecycle and UI.

**Features:**
- Floating notification bell with unread count
- Settings button for preferences
- Automatic event monitoring
- Toast notifications

#### NotificationPreferencesModal.tsx
Settings UI for notification preferences.

**Configurable Events:**
- New proposals
- Voting updates
- Executed proposals
- Cancelled proposals
- Delegation received

**Features:**
- Toggle individual notification types
- Enable/disable all at once
- Persistent storage

#### NotificationDisplay.tsx
Toast notification component that auto-dismisses.

**Features:**
- Auto-dismiss after 6 seconds
- Action links to relevant pages
- Smooth fade-out animation
- Close button

#### NotificationCenterDropdown.tsx
Dropdown notification panel with full history.

**Features:**
- View all notifications
- Mark as read/unread
- Dismiss individual notifications
- Clear all notifications
- Unread count badge

### Hooks

#### useNotifications()
Manages notification state and lifecycle.

```tsx
const {
notifications,
addNotification,
dismissNotification,
markAsRead,
clearAll,
} = useNotifications();
```

#### useContractEvents()
Monitors contract events with callbacks.

```tsx
const { events, isLoading, error, refetch } = useContractEvents({
contractPrincipal: '...',
onNewEvent: (event) => { /* Handle new event */ },
});
```

#### useGovernanceEventNotifications()
Automatically creates notifications from contract events.

```tsx
const { events, isLoading } = useGovernanceEventNotifications({
contractPrincipal: '...',
enabled: true,
});
```

### Services

#### governance-notification-preferences.ts
Manages user notification preferences (localStorage).

```tsx
import {
getGovernanceNotificationPreferences,
saveGovernanceNotificationPreferences,
updateGovernanceNotificationPreference,
} from '@/lib/governance-notification-preferences';
```

#### governance-notifications.ts
Creates notifications from governance events.

```tsx
const notification = createGovernanceNotification('proposalCreated', {
proposalId: '42',
proposalTitle: 'Budget Allocation',
});
```

### Usage

```tsx
export default function RootLayout() {
return (
<Providers>
<GovernanceNotificationManager
contractPrincipal="SP2ZNGJ85ENDY6QTHQ0YCWM1GRFX77YXF1W8F25J9.sprint-fund"
/>
{/* ... rest of app */}
</Providers>
);
}
```

### Integration Points

1. **Root Layout** - Notification manager rendered globally
2. **Dashboard** - Event stream displays recent activity
3. **Header** - Notification bell icon with unread count
4. **User Settings** - Preference management panel

### Data Flow

```
Contract Events (Hiro API)
useContractEvents()
useGovernanceEventNotifications()
Notification Preferences
createGovernanceNotification()
NotificationDisplay (Toast)
NotificationCenterDropdown (Dropdown)
```

### Storage

User notification preferences are stored in localStorage with key `governance-notification-prefs`.

```json
{
"proposalCreated": true,
"proposalVoting": true,
"proposalExecuted": true,
"proposalCancelled": false,
"delegationReceived": true
}
```

---

## Testing

Both features include comprehensive unit tests:

- `contract-events.test.ts` - Event normalization and API fetching
- `governance-notifications.test.ts` - Notification creation and deduplication
- `governance-notification-preferences.test.ts` - Preference storage
- `useContractEvents.test.ts` - Hook behavior and callbacks

Run tests with:
```bash
npm run test
```

---

## API Reference

### Hiro API
Events are fetched from the Stacks API at:
```
https://api.mainnet.hiro.so/extended/v2/addresses/{contractPrincipal}/transactions
```

### Event Structure
```typescript
interface ContractEvent {
id: string;
txId: string;
timestamp: number;
sender: string;
category: 'stake' | 'proposal' | 'vote' | 'cancel' | 'execute' | 'treasury';
status: 'success' | 'failed';
description: string;
amount?: string;
weight?: number;
proposalId?: string;
}
```

---

## Performance Considerations

1. **Polling** - Default 20s for events, 30s for notifications
2. **Deduplication** - Notifications deduplicated within 30s window
3. **Memory** - Events kept in memory; old toasts auto-dismiss
4. **Storage** - Preferences use localStorage (persistent, ~1KB)

---

## Browser Compatibility

- Modern browsers (Chrome, Firefox, Safari, Edge)
- localStorage required for preference persistence
- Requires ES2020+ JavaScript support
96 changes: 96 additions & 0 deletions frontend/components/EventAnalyticsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use client';

import React, { useState } from 'react';
import { useContractEvents } from '../src/hooks/useContractEvents';
import { getEventStats, groupEventsByCategory } from '../src/lib/event-utilities';
import { GOVERNANCE_CONFIG } from '../src/lib/governance-config';
import { BarChart3, TrendingUp } from 'lucide-react';

interface EventAnalyticsPanelProps {
contractPrincipal?: string;
}

export const EventAnalyticsPanel: React.FC<EventAnalyticsPanelProps> = ({
contractPrincipal = GOVERNANCE_CONFIG.CONTRACT_PRINCIPAL,
}) => {
const { events, isLoading } = useContractEvents({
contractPrincipal,
pollInterval: GOVERNANCE_CONFIG.EVENT_POLL_INTERVAL,
});

const stats = getEventStats(events);
const byCategory = groupEventsByCategory(events);

const successRate = stats.total > 0
? ((stats.succeeded / stats.total) * 100).toFixed(1)
: '0.0';

return (
<div className="w-full bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center gap-2 mb-4">
<BarChart3 className="h-5 w-5 text-blue-600" />
<h3 className="text-lg font-semibold text-gray-900">Event Analytics</h3>
</div>

{isLoading && events.length === 0 ? (
<div className="text-sm text-gray-500">Loading analytics...</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-gray-50 rounded-lg p-4">
<p className="text-xs text-gray-600 mb-1">Total Events</p>
<p className="text-2xl font-bold text-gray-900">{stats.total}</p>
</div>

<div className="bg-green-50 rounded-lg p-4">
<p className="text-xs text-gray-600 mb-1">Successful</p>
<p className="text-2xl font-bold text-green-700">{stats.succeeded}</p>
</div>

<div className="bg-red-50 rounded-lg p-4">
<p className="text-xs text-gray-600 mb-1">Failed</p>
<p className="text-2xl font-bold text-red-700">{stats.failed}</p>
</div>

<div className="bg-blue-50 rounded-lg p-4">
<p className="text-xs text-gray-600 mb-1">Success Rate</p>
<p className="text-2xl font-bold text-blue-700">{successRate}%</p>
</div>
</div>
)}
</div>

{byCategory.size > 0 && (
<div className="p-6">
<div className="flex items-center gap-2 mb-4">
<TrendingUp className="h-5 w-5 text-purple-600" />
<h4 className="text-base font-semibold text-gray-900">By Category</h4>
</div>

<div className="space-y-3">
{Array.from(byCategory.entries()).map(([category, catEvents]) => (
<div key={category} className="flex items-center justify-between">
<span className="text-sm text-gray-700 capitalize">{category}</span>
<div className="flex items-center gap-2">
<div className="w-40 bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{
width: `${(catEvents.length / stats.total) * 100}%`,
}}
/>
</div>
<span className="text-sm font-medium text-gray-900 w-12 text-right">
{catEvents.length}
</span>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
};

export default EventAnalyticsPanel;
3 changes: 3 additions & 0 deletions frontend/components/GovernanceAnalyticsDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import ProposerActivityChart from './charts/ProposerActivityChart';
import { AnalyticsKPIPanel } from './dashboard/AnalyticsKPIPanel';
import { AnalyticsExportPanel } from './AnalyticsExportPanel';
import { PerformanceMetricsPanel } from './dashboard/PerformanceMetricsPanel';
import { ContractEventStream } from './dashboard/ContractEventStream';
import FundingMetricsChart from './FundingMetricsChart';
import TreasuryBalanceChart from './charts/TreasuryBalanceChart';
import VoterParticipationTrendChart from './VoterParticipationTrendChart';
Expand Down Expand Up @@ -64,6 +65,8 @@ export default function GovernanceAnalyticsDashboard() {

<AnalyticsKPIPanel />

<ContractEventStream contractPrincipal="SP2ZNGJ85ENDY6QTHQ0YCWM1GRFX77YXF1W8F25J9.sprint-fund" />

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-white/5 backdrop-blur-sm rounded-xl p-6 border border-white/10">
<p className="text-white/60 text-sm mb-2">Total Proposals</p>
Expand Down
Loading
Loading