-
Notifications
You must be signed in to change notification settings - Fork 42
feat: protocol health monitoring dashboard #282
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
estyemma
wants to merge
3
commits into
Stellar-VaultLink:main
Choose a base branch
from
estyemma:health
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| name: health-collector | ||
|
|
||
| # InvoFi protocol health collector. | ||
| # | ||
| # Runs on an hourly schedule to: | ||
| # 1. Poll Soroban RPC getEvents for the past hour across all five contracts | ||
| # and write aggregated success/failure counts + fee stats to | ||
| # Supabase `health_metrics`. | ||
| # 2. Snapshot current contract state (invoice distribution, insurance pool, | ||
| # active lenders) to `contract_state_snapshots`. | ||
| # 3. Evaluate threshold-based `alert_configs` and append breaches to | ||
| # `audit_log`. | ||
| # | ||
| # The /dashboard/health admin page reads all three tables to render the | ||
| # real-time protocol health monitoring view. | ||
| # | ||
| # Required repo configuration (once): | ||
| # Secrets: | ||
| # SUPABASE_URL — your Supabase project URL | ||
| # SUPABASE_SERVICE_ROLE_KEY — service role key (bypasses RLS for writes) | ||
| # Variables (defaults point to the live testnet deployment): | ||
| # REGISTRY_CONTRACT_ID | ||
| # FINANCING_CONTRACT_ID | ||
| # REPAYMENT_CONTRACT_ID | ||
| # INSURANCE_CONTRACT_ID | ||
| # REPUTATION_CONTRACT_ID | ||
| # | ||
| # Optional secrets/variables: | ||
| # HEALTH_RPC_URL — override the Soroban RPC endpoint | ||
| # HEALTH_LOOKBACK_HOURS — hours to look back (default: 1) | ||
| # | ||
| # Enable/disable on demand: | ||
| # gh workflow enable health-collector.yml -R Stellar-VaultLink/invofi | ||
| # gh workflow disable health-collector.yml -R Stellar-VaultLink/invofi | ||
|
|
||
| on: | ||
| schedule: | ||
| # Every hour at minute 45 (UTC) — offset from the keeper (minute 0) and | ||
| # the indexer (minute 15) to spread load. | ||
| - cron: '45 * * * *' | ||
| workflow_dispatch: | ||
| inputs: | ||
| lookback_hours: | ||
| description: 'Hours to look back for events (default: 1)' | ||
| required: false | ||
| default: '1' | ||
| dry_run: | ||
| description: 'Print collected data but do not write to Supabase' | ||
| type: boolean | ||
| required: false | ||
| default: false | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| collect: | ||
| name: Health collector / testnet | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
|
|
||
| defaults: | ||
| run: | ||
| working-directory: invofi/scripts | ||
|
|
||
| steps: | ||
| # ── Checkout ────────────────────────────────────────────────────────── | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4 | ||
|
|
||
| # ── Node.js ─────────────────────────────────────────────────────────── | ||
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v4 | ||
| with: | ||
| # Node 22+ required: @supabase/supabase-js@2.112+ and | ||
| # @stellar/stellar-sdk@16.2+ declare engines node >=22. | ||
| node-version: 22 | ||
| cache: npm | ||
| cache-dependency-path: invofi/scripts/package-lock.json | ||
|
|
||
| # ── Install dependencies ────────────────────────────────────────────── | ||
| - name: Install dependencies | ||
| run: npm ci | ||
|
|
||
| # ── Type-check ──────────────────────────────────────────────────────── | ||
| - name: Type-check | ||
| run: npm run type-check | ||
|
|
||
| # ── Run unit tests ──────────────────────────────────────────────────── | ||
| - name: Run unit tests | ||
| run: npm run test:health | ||
|
|
||
| # ── Run collector ───────────────────────────────────────────────────── | ||
| - name: Run health collector | ||
| env: | ||
| RPC_URL: ${{ vars.HEALTH_RPC_URL || 'https://soroban-testnet.stellar.org' }} | ||
| NETWORK_PASSPHRASE: Test SDF Network ; September 2015 | ||
| REGISTRY_CONTRACT_ID: ${{ vars.REGISTRY_CONTRACT_ID || 'CAXNTWSKDVSB3GPJMU3RTSDTAIFF4A6FFRAAI35B4AE7LZLLI4VXMCF7' }} | ||
| FINANCING_CONTRACT_ID: ${{ vars.FINANCING_CONTRACT_ID || 'CBGRA3457ZFXYZNEQLO4YGUQ3OBEWOE6US6ZREHK6NF2DLZYBO73IFVW' }} | ||
| REPAYMENT_CONTRACT_ID: ${{ vars.REPAYMENT_CONTRACT_ID || 'CCDATW5GMVDOPK55Q4MLXV5SGA3VLXPD67ABLBNMHWFF6BLL2IZBUVEP' }} | ||
| INSURANCE_CONTRACT_ID: ${{ vars.INSURANCE_CONTRACT_ID || 'CAURQCGDZZ6PPCH6EKDVQP5W372CH3PQ62VQC2GKLIXNHB37VOMBMSU5' }} | ||
| REPUTATION_CONTRACT_ID: ${{ vars.REPUTATION_CONTRACT_ID || 'CCHKVUWGTQ56U53C5U7ZSOFDTTMGLMOFCL22DME5UMXIYWQNUYXOYPDN' }} | ||
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | ||
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | ||
| LOOKBACK_HOURS: ${{ github.event.inputs.lookback_hours || vars.HEALTH_LOOKBACK_HOURS || '1' }} | ||
| DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }} | ||
| run: npm run health-collector | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| # Protocol Health Monitoring Dashboard | ||
|
|
||
| ## Problem | ||
|
|
||
| InvoFi's public `/stats` page gives aggregate totals (total invoices, total volume, | ||
| repayment rate) but these are 6-hour snapshots from the indexer. There is no operational | ||
| view for protocol maintainers — no transaction success/failure breakdown, no contract | ||
| pause indicator, no alerting when the overdue rate spikes, and no audit trail of admin | ||
| actions. When something goes wrong on-chain, the only recourse is to manually query | ||
| Stellar Expert or grep GitHub Action logs. | ||
|
|
||
|
|
||
| Concretely: | ||
|
|
||
| - A lender watching their offer go stale has no visibility into *why* — is the overdue | ||
| rate normal? Is the insurance pool healthy? | ||
| - An admin who ran `mark_overdue` or `resolve_dispute` leaves no in-app audit trail. | ||
| - There is no threshold mechanism to page someone when `overdue / financed > 15%`. | ||
| - Gas consumption and confirmation-time outliers are invisible. | ||
|
|
||
| --- | ||
|
|
||
| ## Solution Approach | ||
|
|
||
| ### Architectural decisions | ||
|
|
||
| **No new always-on server.** The indexer already runs as a scheduled GitHub Action every | ||
| 6 hours. We extend the same pattern: a lightweight GitHub Action (or Supabase Edge | ||
| Function) collects health metrics on a schedule, stores them in Supabase, and the | ||
| dashboard reads them. All hosting stays free. | ||
|
|
||
| **Admin role via `user_profiles.role`.** The existing `user_profiles` table already has a | ||
| `role` text column (`business | lender`). We extend the `CHECK` constraint to also allow | ||
| `admin` and add a server-side guard that redirects non-admin users to `/403`. | ||
|
|
||
| **Pure-SVG sparkline charts.** The codebase has no chart library. Rather than pulling in | ||
| `recharts` (adds ~300 KB to the bundle), we build a tiny reusable `<Sparkline>` SVG | ||
| component and a `<BarChart>` SVG component. They are sufficient for line trends and | ||
| distribution bars, and they have zero dependencies. If stakeholders later want richer | ||
| interactivity, `recharts` can be layered on top. | ||
|
|
||
| **Supabase tables as the metrics store.** Four new tables: | ||
| - `health_metrics` — one row per time bucket (hourly), with success/failure counts, | ||
| avg confirmation time, and gas estimates. Written by the collector script. | ||
| - `contract_state_snapshots` — one row per 6-hour run, capturing invoice status | ||
| distribution, pool utilisation, and position token supply. | ||
| - `alert_configs` — admin-managed threshold rules (e.g. `overdue_rate > 0.15`). | ||
| - `audit_log` — append-only log of admin actions taken through the app. | ||
|
|
||
| **Data collection via GitHub Actions.** The existing `indexer.yml` workflow is already | ||
| triggered on schedule. We add a companion `health-collector.yml` that runs hourly, | ||
| calls Soroban RPC `getEvents`, writes to `health_metrics`, and computes | ||
| `contract_state_snapshots`. The frontend dashboard is a pure reader — no server-side | ||
| API route required. | ||
|
|
||
| ### File layout | ||
|
|
||
| ``` | ||
| src/ | ||
| ├── app/ | ||
| │ └── dashboard/ | ||
| │ └── health/ | ||
| │ ├── page.tsx ← main dashboard, admin-gated | ||
| │ └── layout.tsx ← layout wrapper | ||
| ├── components/ | ||
| │ └── health/ | ||
| │ ├── TxRateChart.tsx ← SVG sparkline: success/failure rates | ||
| │ ├── ContractStateCards.tsx ← KPI cards: invoices, pool util, overdue | ||
| │ ├── AlertConfigPanel.tsx ← threshold editor | ||
| │ └── AuditLogViewer.tsx ← paginated audit log table | ||
| └── lib/ | ||
| ├── health/ | ||
| │ ├── metrics.ts ← Supabase read/write helpers | ||
| │ ├── collector.ts ← Soroban RPC event ingestion | ||
| │ └── types.ts ← TypeScript types for all health tables | ||
| └── migrations/ | ||
| └── 004_health_monitoring.sql | ||
| ``` | ||
|
|
||
| ### Implementation steps | ||
|
|
||
| 1. **Migration** (`004_health_monitoring.sql`): create the four tables with RLS. | ||
| Admin-only write on `alert_configs`; public read on `health_metrics` and | ||
| `contract_state_snapshots`; authenticated read on `audit_log`. | ||
|
|
||
| 2. **Types and helpers** (`lib/health/`): typed Supabase helpers for reading time-series | ||
| data with a time-range filter (`1h | 24h | 7d | 30d`). | ||
|
|
||
| 3. **Admin gate** (`components/health/AdminGuard.tsx`): wraps the page; reads | ||
| `user_profiles.role` after auth check; redirects to `/403` if not `admin`. | ||
|
|
||
| 4. **Chart components**: `<Sparkline>` (polyline SVG, responsive via viewBox), | ||
| `<TxRateChart>` (stacked success/failure bars), both zero-dependency. | ||
|
|
||
| 5. **Dashboard page** (`app/dashboard/health/page.tsx`): `<AdminGuard>` wrapper, | ||
| time-range selector (tabs), four sections: KPI cards, transaction rate chart, | ||
| alert config panel, audit log. | ||
|
|
||
| 6. **Alert config panel**: reads `alert_configs`, lets admins set thresholds, writes | ||
| back via Supabase insert/update. A separate `checkAlerts()` utility (called by | ||
| the collector) evaluates thresholds and inserts into `audit_log` when breached. | ||
|
|
||
| 7. **Audit log viewer**: paginated table of `audit_log` rows with filtering by action | ||
| type and time range. Supports CSV export via the existing `toCsv / downloadCsv` | ||
| helpers in `lib/csv.ts`. | ||
|
|
||
| 8. **CSV export**: reuses `toCsv` / `downloadCsv` from `lib/csv.ts` with | ||
| dashboard-specific column specs. | ||
|
|
||
| ### Acceptance criteria mapping | ||
|
|
||
| | Criterion | Solution | | ||
| |---|---| | ||
| | `/dashboard/health` admin-only | `AdminGuard` checks `user_profiles.role = 'admin'`; redirects to `/403` | | ||
| | Real-time tx success rate chart | `TxRateChart` reads `health_metrics`, auto-refreshes every 60 s | | ||
| | Contract state summary cards | `ContractStateCards` reads `contract_state_snapshots` | | ||
| | Alert config panel | `AlertConfigPanel` reads/writes `alert_configs` | | ||
| | Audit log viewer | `AuditLogViewer` reads `audit_log` with pagination | | ||
| | CSV export | "Export CSV" button in both metrics and audit log sections | | ||
| | Time-range filtering | `TimeRangeSelector` controls a `since` timestamp passed to all queries | | ||
| | Responsive layout | Tailwind responsive grid identical to existing stats page | | ||
|
|
||
| ### What is not included (and why) | ||
|
|
||
| - **Gas consumption** is not exposed by Soroban RPC's public API — `getTransaction` | ||
| returns fee but not gas units. We track *fee* as a proxy in `health_metrics.avg_fee_stroops`. | ||
| - **WebSocket real-time push** is intentionally omitted (the live portfolio dashboard | ||
| already covers that complexity). The health page polls on a 60-second interval, which | ||
| is sufficient for operations monitoring without adding WebSocket infrastructure. | ||
| - **Recharts / charting library** is intentionally not added to avoid a large bundle | ||
| dependency. The SVG approach is sufficient and auditable; a migration path to recharts | ||
| is straightforward if ever needed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import type { Metadata } from 'next'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Protocol Health — InvoFi', | ||
| description: | ||
| 'Real-time protocol health monitoring dashboard for InvoFi maintainers. Admin access only.', | ||
| // Prevent search engines from indexing the admin dashboard. | ||
| robots: { index: false, follow: false }, | ||
| }; | ||
|
|
||
| export default function HealthDashboardLayout({ | ||
| children, | ||
| }: { | ||
| children: React.ReactNode; | ||
| }) { | ||
| return <>{children}</>; | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Set
persist-credentials: falseon checkout.The job does not push or use the Git credential after checkout.
actions/checkoutwrites theGITHUB_TOKENinto.git/configby default, where any later step or dependency script can read it. This job installs npm dependencies and runs a collector with the Supabase service-role key, so limit the exposed credential surface.🔒 Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 zizmor (1.29.0)
[warning] 68-70: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Source: Linters/SAST tools