-
Notifications
You must be signed in to change notification settings - Fork 90
feat: forester dashboard #2269
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
feat: forester dashboard #2269
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ logs | |
| .idea | ||
| .env | ||
| .env.devnet | ||
| .env.mainnet | ||
| *.json | ||
| !package.json | ||
| spawn.sh | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| NEXT_PUBLIC_FORESTER_API_URL=http://localhost:8080 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| node_modules/ | ||
| .next/ | ||
| out/ | ||
| .env.local |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||||||||||||||||||||
| FROM node:20-alpine AS base | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| FROM base AS deps | ||||||||||||||||||||||
| WORKDIR /app | ||||||||||||||||||||||
| COPY package.json package-lock.json* ./ | ||||||||||||||||||||||
| RUN npm ci | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| FROM base AS builder | ||||||||||||||||||||||
| WORKDIR /app | ||||||||||||||||||||||
| COPY --from=deps /app/node_modules ./node_modules | ||||||||||||||||||||||
| COPY . . | ||||||||||||||||||||||
| RUN npm run build | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| FROM base AS runner | ||||||||||||||||||||||
| WORKDIR /app | ||||||||||||||||||||||
| ENV NODE_ENV=production | ||||||||||||||||||||||
| RUN addgroup --system --gid 1001 nodejs | ||||||||||||||||||||||
| RUN adduser --system --uid 1001 nextjs | ||||||||||||||||||||||
| COPY --from=builder /app/public ./public | ||||||||||||||||||||||
| COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ | ||||||||||||||||||||||
| COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| USER nextjs | ||||||||||||||||||||||
| EXPOSE 3000 | ||||||||||||||||||||||
| ENV PORT=3000 | ||||||||||||||||||||||
| CMD ["node", "server.js"] | ||||||||||||||||||||||
|
Comment on lines
+23
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding a Both Trivy and Checkov flag the missing 🩺 Example HEALTHCHECK USER nextjs
EXPOSE 3000
ENV PORT=3000
+HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
+ CMD ["wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/", "||", "exit", "1"]
CMD ["node", "server.js"]Note: 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Checkov (3.2.334)[low] 1-26: Ensure that HEALTHCHECK instructions have been added to container images (CKV_DOCKER_2) 🤖 Prompt for AI Agents |
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| /// <reference types="next" /> | ||
| /// <reference types="next/image-types/global" /> | ||
|
|
||
| // NOTE: This file should not be edited | ||
| // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| /** @type {import('next').NextConfig} */ | ||
| const nextConfig = { | ||
| output: "standalone", | ||
| }; | ||
|
|
||
| export default nextConfig; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| { | ||
| "name": "forester-dashboard", | ||
| "version": "0.1.0", | ||
| "private": true, | ||
| "scripts": { | ||
| "dev": "next dev", | ||
| "build": "next build", | ||
| "start": "next start", | ||
| "lint": "next lint" | ||
| }, | ||
| "dependencies": { | ||
| "next": "^14.2.0", | ||
| "react": "^18.3.0", | ||
| "react-dom": "^18.3.0", | ||
| "swr": "^2.2.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^20.0.0", | ||
| "@types/react": "^18.3.0", | ||
| "@types/react-dom": "^18.3.0", | ||
| "autoprefixer": "^10.4.0", | ||
| "postcss": "^8.4.0", | ||
| "tailwindcss": "^3.4.0", | ||
| "typescript": "^5.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /** @type {import('postcss-load-config').Config} */ | ||
| const config = { | ||
| plugins: { | ||
| tailwindcss: {}, | ||
| autoprefixer: {}, | ||
| }, | ||
| }; | ||
|
|
||
| export default config; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| "use client"; | ||
|
|
||
| import { useCompressible } from "@/hooks/useCompressible"; | ||
| import { ErrorState } from "@/components/ErrorState"; | ||
| import { CompressiblePanel } from "@/components/CompressiblePanel"; | ||
|
|
||
| export default function CompressiblePage() { | ||
| const { data, error, isLoading } = useCompressible(); | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div className="flex items-center justify-center h-64"> | ||
| <div className="text-gray-400 text-sm"> | ||
| Loading compressible status... | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (error || !data) { | ||
| return <ErrorState error={error} fallbackMessage="Failed to load compressible data" />; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="space-y-4"> | ||
| <h2 className="text-xl font-bold">Compressible Accounts</h2> | ||
| <CompressiblePanel data={data} /> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| @tailwind base; | ||
| @tailwind components; | ||
| @tailwind utilities; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import type { Metadata } from "next"; | ||
| import { Sidebar } from "@/components/Sidebar"; | ||
| import "./globals.css"; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: "Forester Dashboard", | ||
| description: "Light Protocol Forester monitoring dashboard", | ||
| }; | ||
|
|
||
| export default function RootLayout({ | ||
| children, | ||
| }: { | ||
| children: React.ReactNode; | ||
| }) { | ||
| return ( | ||
| <html lang="en"> | ||
| <body className="bg-white text-gray-900 antialiased"> | ||
| <div className="flex min-h-screen"> | ||
| <Sidebar /> | ||
| <main className="flex-1 p-6 overflow-auto">{children}</main> | ||
| </div> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| "use client"; | ||
|
|
||
| import { useMetrics } from "@/hooks/useMetrics"; | ||
| import { ErrorState } from "@/components/ErrorState"; | ||
| import { MetricsPanel } from "@/components/MetricsPanel"; | ||
|
|
||
| export default function MetricsPage() { | ||
| const { data: metrics, error, isLoading } = useMetrics(); | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div className="flex items-center justify-center h-64"> | ||
| <div className="text-gray-400 text-sm">Loading metrics...</div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (error || !metrics) { | ||
| return <ErrorState error={error} fallbackMessage="Failed to load metrics" />; | ||
| } | ||
|
|
||
| const isEmpty = | ||
| Object.keys(metrics.transactions_processed_total).length === 0 && | ||
| Object.keys(metrics.forester_balances).length === 0; | ||
|
Comment on lines
+22
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider including The emptiness check only looks at 🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <div className="space-y-4"> | ||
| <h2 className="text-xl font-bold">Metrics</h2> | ||
| {isEmpty ? ( | ||
| <div className="bg-white rounded-lg border border-gray-200 p-8 text-center"> | ||
| <p className="text-gray-500 text-sm"> | ||
| No metrics data available. | ||
| </p> | ||
| <p className="text-gray-400 text-xs mt-2"> | ||
| Configure --prometheus-url to query aggregated metrics, or connect | ||
| to a running forester instance. | ||
| </p> | ||
| </div> | ||
| ) : ( | ||
| <MetricsPanel metrics={metrics} /> | ||
| )} | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,103 @@ | ||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import { useForesterStatus } from "@/hooks/useForesterStatus"; | ||||||||||||||||||||||||||||||||||
| import { EpochCard } from "@/components/EpochCard"; | ||||||||||||||||||||||||||||||||||
| import { ErrorState } from "@/components/ErrorState"; | ||||||||||||||||||||||||||||||||||
| import { ForesterList } from "@/components/ForesterList"; | ||||||||||||||||||||||||||||||||||
| import { QueuePressureChart } from "@/components/QueuePressureChart"; | ||||||||||||||||||||||||||||||||||
| import { formatNumber } from "@/lib/utils"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| export default function OverviewPage() { | ||||||||||||||||||||||||||||||||||
| const { data: status, error, isLoading } = useForesterStatus(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (isLoading) { | ||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-center h-64"> | ||||||||||||||||||||||||||||||||||
| <div className="text-gray-400 text-sm">Loading forester status...</div> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (error || !status) { | ||||||||||||||||||||||||||||||||||
| return <ErrorState error={error} fallbackMessage="Failed to load forester status" />; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const warnings: string[] = []; | ||||||||||||||||||||||||||||||||||
| if (status.active_epoch_foresters.length === 0) { | ||||||||||||||||||||||||||||||||||
| warnings.push("No foresters registered for the active epoch"); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||
| status.slots_until_next_registration < 1000 && | ||||||||||||||||||||||||||||||||||
| status.registration_epoch_foresters.length === 0 | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| warnings.push("Registration closing soon with no foresters registered"); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Magic number This threshold controls when the "Registration closing soon" warning appears. Consider extracting it as a named constant to make the intent clearer and easier to tune: +const REGISTRATION_WARNING_SLOT_THRESHOLD = 1000;
+
if (
- status.slots_until_next_registration < 1000 &&
+ status.slots_until_next_registration < REGISTRATION_WARNING_SLOT_THRESHOLD &&
status.registration_epoch_foresters.length === 0
)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <div className="space-y-6"> | ||||||||||||||||||||||||||||||||||
| <div className="flex items-center justify-between"> | ||||||||||||||||||||||||||||||||||
| <h2 className="text-xl font-bold">Overview</h2> | ||||||||||||||||||||||||||||||||||
| <span className="text-xs text-gray-400 font-mono"> | ||||||||||||||||||||||||||||||||||
| Slot {formatNumber(status.slot)} | ||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| {warnings.map((w, i) => ( | ||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||
| key={i} | ||||||||||||||||||||||||||||||||||
| className="bg-amber-50 border border-amber-200 rounded-lg px-4 py-3 text-sm text-amber-800" | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| {w} | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider using the warning string as key instead of array index. Since warning messages are unique strings derived deterministically from the status, they make better React keys than array indices. This is a minor nit — with a small, non-reorderable list it won't cause bugs, but it's slightly more idiomatic: - {warnings.map((w, i) => (
- <div key={i}
+ {warnings.map((w) => (
+ <div key={w}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> | ||||||||||||||||||||||||||||||||||
| <StatCard label="Total Trees" value={status.total_trees} /> | ||||||||||||||||||||||||||||||||||
| <StatCard label="Active Trees" value={status.active_trees} /> | ||||||||||||||||||||||||||||||||||
| <StatCard label="Rolled Over" value={status.rolled_over_trees} /> | ||||||||||||||||||||||||||||||||||
| <StatCard | ||||||||||||||||||||||||||||||||||
| label="Total Pending" | ||||||||||||||||||||||||||||||||||
| value={status.total_pending_items} | ||||||||||||||||||||||||||||||||||
| highlight={status.total_pending_items > 0} | ||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | ||||||||||||||||||||||||||||||||||
| <EpochCard status={status} /> | ||||||||||||||||||||||||||||||||||
| <QueuePressureChart stats={status.aggregate_queue_stats} /> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | ||||||||||||||||||||||||||||||||||
| <ForesterList | ||||||||||||||||||||||||||||||||||
| title="Active Epoch Foresters" | ||||||||||||||||||||||||||||||||||
| foresters={status.active_epoch_foresters} | ||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||
| <ForesterList | ||||||||||||||||||||||||||||||||||
| title="Registration Epoch Foresters" | ||||||||||||||||||||||||||||||||||
| foresters={status.registration_epoch_foresters} | ||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| function StatCard({ | ||||||||||||||||||||||||||||||||||
| label, | ||||||||||||||||||||||||||||||||||
| value, | ||||||||||||||||||||||||||||||||||
| highlight, | ||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||
| label: string; | ||||||||||||||||||||||||||||||||||
| value: number; | ||||||||||||||||||||||||||||||||||
| highlight?: boolean; | ||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||
| <div className="bg-white rounded-lg border border-gray-200 p-4"> | ||||||||||||||||||||||||||||||||||
| <div className="text-xs text-gray-500">{label}</div> | ||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||
| className={`text-2xl font-semibold mt-1 ${highlight ? "text-amber-600" : "text-gray-900"}`} | ||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||
| {formatNumber(value)} | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| "use client"; | ||
|
|
||
| import { useForesterStatus } from "@/hooks/useForesterStatus"; | ||
| import { ErrorState } from "@/components/ErrorState"; | ||
| import { TreeTable } from "@/components/TreeTable"; | ||
|
|
||
| export default function TreesPage() { | ||
| const { data: status, error, isLoading } = useForesterStatus(); | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div className="flex items-center justify-center h-64"> | ||
| <div className="text-gray-400 text-sm">Loading trees...</div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (error || !status) { | ||
| return <ErrorState error={error} fallbackMessage="Failed to load trees" />; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="space-y-4"> | ||
| <h2 className="text-xl font-bold">Trees</h2> | ||
| <TreeTable | ||
| trees={status.trees} | ||
| foresters={status.active_epoch_foresters} | ||
| currentLightSlot={status.current_light_slot} | ||
| /> | ||
| </div> | ||
| ); | ||
| } |
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.
🧹 Nitpick | 🔵 Trivial
Consolidate consecutive
RUNinstructions.These two
RUNcommands can be merged into one layer, reducing image size slightly and addressing the Hadolint DL3059 hint.♻️ Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 Hadolint (2.14.0)
[info] 18-18: Multiple consecutive
RUNinstructions. Consider consolidation.(DL3059)
🤖 Prompt for AI Agents