-
Notifications
You must be signed in to change notification settings - Fork 68
feat: implement reconciliation service worker and monitoring dashboar… #345
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
base: main
Are you sure you want to change the base?
Changes from all commits
e99eda6
bdd98e0
03d728e
40741c1
2dc585d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,5 @@ | ||
| "use client"; | ||
|
|
||
| import { SiteShell } from "@/components/site-shell"; | ||
| import { RoleOverview } from "@/components/dashboard/role-overview"; | ||
| import { ClientDashboard } from "@/components/dashboard/client-dashboard"; | ||
| import { useAuthStore } from "@/lib/store/use-auth-store"; | ||
| import { WalletConnect } from "@/components/WalletConnect"; | ||
| import ReconciliationDashboard from "@/components/ReconciliationDashboard"; | ||
|
|
||
| export default function Home() { | ||
| const { role, isLoggedIn } = useAuthStore(); | ||
|
|
||
| const eyebrow = isLoggedIn ? `${role} cockpit` : "Stellar Freelance Infrastructure"; | ||
| const title = role === 'client' ? "Manage hiring and escrow milestones with absolute clarity." : "Premium freelance execution with escrow, verifiable reputation, and transparent AI arbitration."; | ||
|
|
||
| return ( | ||
| <SiteShell | ||
| eyebrow={eyebrow} | ||
| title={title} | ||
| description="Lance is the surface layer for serious clients and elite independents who want payment security, immutable trust signals, and fast dispute resolution." | ||
| > | ||
| {!isLoggedIn && ( | ||
| <div className="mb-12 flex justify-center"> | ||
| <WalletConnect /> | ||
| </div> | ||
| )} | ||
| {role === "client" ? <ClientDashboard /> : <RoleOverview />} | ||
| </SiteShell> | ||
| ); | ||
| return <ReconciliationDashboard />; | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,250 @@ | ||||||
| "use client"; | ||||||
|
|
||||||
| import React, { useEffect, useMemo, useState } from "react"; | ||||||
| import { | ||||||
| LineChart, | ||||||
| Line, | ||||||
| XAxis, | ||||||
| YAxis, | ||||||
| Tooltip, | ||||||
| ResponsiveContainer, | ||||||
| AreaChart, | ||||||
| Area, | ||||||
| CartesianGrid, | ||||||
| } from "recharts"; | ||||||
| import { CheckCircle, AlertCircle, RefreshCcw, Play } from "lucide-react"; | ||||||
|
|
||||||
| type ThroughputPoint = { t: string; indexed: number }; | ||||||
| type ResourcePoint = { t: string; cpu: number; mem: number }; | ||||||
| type EventRow = { id: number; ledger: string; event: string; ts: string }; | ||||||
|
|
||||||
| export default function ReconciliationDashboard(): React.ReactElement { | ||||||
| const [throughput, setThroughput] = useState<ThroughputPoint[]>(() => seedThroughput()); | ||||||
| const [resources, setResources] = useState<ResourcePoint[]>(() => seedResources()); | ||||||
| const [events, setEvents] = useState<EventRow[]>(() => seedEvents()); | ||||||
| const [statusHealthy, setStatusHealthy] = useState(true); | ||||||
| const [actionMsg, setActionMsg] = useState<string | null>(null); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| const t = setInterval(() => { | ||||||
| setThroughput((prev) => { | ||||||
| const next = [...prev.slice(-29), randomThroughputPoint()]; | ||||||
| return next; | ||||||
| }); | ||||||
|
|
||||||
| setResources((prev) => { | ||||||
| const next = [...prev.slice(-29), randomResourcePoint()]; | ||||||
| return next; | ||||||
| }); | ||||||
|
|
||||||
| setEvents((prev) => { | ||||||
| const now = new Date(); | ||||||
| const nextEvent: EventRow = { | ||||||
| id: prev.length + 1, | ||||||
| ledger: (Math.floor(Math.random() * 1_000_000) + 100000).toString(), | ||||||
| event: Math.random() > 0.8 ? "error_event" : "indexed_event", | ||||||
| ts: now.toISOString(), | ||||||
| }; | ||||||
| const next = [nextEvent, ...prev].slice(0, 20); | ||||||
| return next; | ||||||
| }); | ||||||
|
|
||||||
| // occasionally flip health | ||||||
| if (Math.random() > 0.97) setStatusHealthy((s) => !s); | ||||||
| }, 1500); | ||||||
|
|
||||||
| return () => clearInterval(t); | ||||||
| }, []); | ||||||
|
|
||||||
| const latestLedger = useMemo(() => throughput[throughput.length - 1]?.indexed ?? 0, [throughput]); | ||||||
|
|
||||||
| function onRestart() { | ||||||
| const ok = window.confirm("Are you sure you want to restart the indexer?"); | ||||||
| if (!ok) return; | ||||||
| setActionMsg("Restarting indexer..."); | ||||||
| setTimeout(() => setActionMsg("Indexer restarted"), 900); | ||||||
| setTimeout(() => setActionMsg(null), 1800); | ||||||
| } | ||||||
|
|
||||||
| function onRescan() { | ||||||
| const ok = window.confirm("Trigger ledger re-scan from checkpoint? This may re-process many ledgers."); | ||||||
| if (!ok) return; | ||||||
| setActionMsg("Starting ledger re-scan..."); | ||||||
| setTimeout(() => setActionMsg("Re-scan queued"), 1200); | ||||||
| setTimeout(() => setActionMsg(null), 2800); | ||||||
| } | ||||||
|
|
||||||
| return ( | ||||||
| <div className="min-h-screen p-6 bg-zinc-950 text-zinc-300 font-sans"> | ||||||
| <div className="max-w-7xl mx-auto"> | ||||||
| <header className="flex items-center justify-between mb-4"> | ||||||
| <h1 className="text-lg font-semibold">Reconciliation — Monitoring</h1> | ||||||
| <div className="flex items-center gap-3"> | ||||||
| <div className="flex items-center gap-2"> | ||||||
| {statusHealthy ? ( | ||||||
| <CheckCircle className="text-green-400" /> | ||||||
| ) : ( | ||||||
| <AlertCircle className="text-rose-500" /> | ||||||
| )} | ||||||
| <span className="text-xs font-mono">{statusHealthy ? "Healthy" : "Degraded"}</span> | ||||||
| </div> | ||||||
|
|
||||||
| <button | ||||||
| onClick={onRestart} | ||||||
| className="inline-flex items-center gap-2 px-3 py-1 bg-zinc-900 border border-zinc-800 text-xs rounded text-zinc-200 hover:bg-zinc-800" | ||||||
| > | ||||||
| <RefreshCcw className="w-4 h-4" /> Restart | ||||||
| </button> | ||||||
|
|
||||||
| <button | ||||||
| onClick={onRescan} | ||||||
| className="inline-flex items-center gap-2 px-3 py-1 bg-zinc-900 border border-zinc-800 text-xs rounded text-zinc-200 hover:bg-zinc-800" | ||||||
| > | ||||||
| <Play className="w-4 h-4" /> Rescan | ||||||
| </button> | ||||||
| </div> | ||||||
| </header> | ||||||
|
|
||||||
| {actionMsg && ( | ||||||
| <div className="mb-4 p-2 bg-zinc-900 border border-zinc-800 rounded text-sm">{actionMsg}</div> | ||||||
| )} | ||||||
|
|
||||||
| <section className="grid grid-cols-12 gap-4"> | ||||||
| <div className="col-span-7 bg-zinc-900 border border-zinc-800 rounded p-3"> | ||||||
| <div className="flex items-baseline justify-between mb-2"> | ||||||
| <div> | ||||||
| <div className="text-xs text-zinc-400">Latest processed ledger</div> | ||||||
| <div className="text-2xl font-mono">{latestLedger}</div> | ||||||
| </div> | ||||||
| <div className="text-right"> | ||||||
| <div className="text-xs text-zinc-400">Throughput (ledgers/s)</div> | ||||||
| <div className="text-sm font-mono">{Math.round(throughput.slice(-5).reduce((s, p) => s + p.indexed, 0) / 5 || 0)}</div> | ||||||
| </div> | ||||||
| </div> | ||||||
|
|
||||||
| <div className="h-44"> | ||||||
| <ResponsiveContainer width="100%" height="100%"> | ||||||
| <LineChart data={throughput} margin={{ top: 6, right: 12, left: 0, bottom: 6 }}> | ||||||
| <CartesianGrid stroke="#111827" strokeDasharray="3 3" /> | ||||||
| <XAxis dataKey="t" tick={{ fill: "#9CA3AF", fontSize: 10 }} /> | ||||||
| <YAxis tick={{ fill: "#9CA3AF", fontSize: 10 }} /> | ||||||
| <Tooltip wrapperStyle={{ background: "#0b0b0b", borderRadius: 4 }} /> | ||||||
| <Line type="monotone" dataKey="indexed" stroke="#10B981" strokeWidth={2} dot={false} isAnimationActive={true} animationDuration={400} /> | ||||||
| </LineChart> | ||||||
| </ResponsiveContainer> | ||||||
| </div> | ||||||
|
|
||||||
| <div className="mt-3 grid grid-cols-2 gap-3"> | ||||||
| <div className="bg-zinc-950 border border-zinc-800 rounded p-2 text-xs"> | ||||||
| <div className="text-zinc-400 mb-1">Indexer Uptime</div> | ||||||
| <div className="font-mono text-sm">3 days 12:34:11</div> | ||||||
| </div> | ||||||
| <div className="bg-zinc-950 border border-zinc-800 rounded p-2 text-xs"> | ||||||
| <div className="text-zinc-400 mb-1">Last Success</div> | ||||||
| <div className="font-mono text-sm">{new Date().toISOString()}</div> | ||||||
|
||||||
| <div className="font-mono text-sm">{new Date().toISOString()}</div> | |
| <div className="font-mono text-sm" suppressHydrationWarning>{new Date().toISOString()}</div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| [package] | ||
| name = "reconciliation-service" | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
|
|
||
| [[bin]] | ||
| name = "reconciliation-service" | ||
| path = "src/main.rs" | ||
|
|
||
| [dependencies] | ||
| anyhow = { workspace = true } | ||
| axum = { workspace = true } | ||
| chrono = { workspace = true } | ||
| dotenvy = { workspace = true } | ||
| prometheus = { workspace = true } | ||
| reqwest = { workspace = true } | ||
| serde = { workspace = true } | ||
| serde_json = { workspace = true } | ||
| sqlx = { workspace = true } | ||
| thiserror = { workspace = true } | ||
| tokio = { workspace = true } | ||
| tower = { workspace = true } | ||
| tower-http = { workspace = true } | ||
| tracing = { workspace = true } | ||
| tracing-subscriber = { workspace = true } | ||
|
|
||
| [dev-dependencies] | ||
| wiremock = "0.6" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| FROM rust:1.88-bookworm AS builder | ||
|
|
||
| WORKDIR /workspace | ||
|
|
||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends pkg-config libssl-dev ca-certificates \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| COPY . . | ||
|
|
||
| RUN cargo build --release --manifest-path backend/reconciliation-service/Cargo.toml | ||
|
|
||
| FROM debian:bookworm-slim AS runtime | ||
|
|
||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends ca-certificates \ | ||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && useradd --system --create-home --uid 10001 reconciliation | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY --from=builder /workspace/target/release/reconciliation-service /usr/local/bin/reconciliation-service | ||
|
|
||
| EXPOSE 3000 | ||
|
|
||
| USER reconciliation | ||
|
|
||
| CMD ["reconciliation-service"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| CREATE TABLE IF NOT EXISTS ledger_checkpoints ( | ||
| id SMALLINT PRIMARY KEY, | ||
| last_processed_ledger BIGINT NOT NULL DEFAULT 0, | ||
| updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), | ||
| CONSTRAINT ledger_checkpoints_single_row CHECK (id = 1) | ||
| ); | ||
|
|
||
| INSERT INTO ledger_checkpoints (id, last_processed_ledger) | ||
| VALUES (1, 0) | ||
| ON CONFLICT (id) DO NOTHING; | ||
|
|
||
| CREATE TABLE IF NOT EXISTS indexed_events ( | ||
| id BIGSERIAL PRIMARY KEY, | ||
| event_key TEXT NOT NULL UNIQUE, | ||
| ledger_sequence BIGINT NOT NULL, | ||
| event_type TEXT NOT NULL, | ||
| payload JSONB NOT NULL, | ||
| processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() | ||
| ); | ||
|
|
||
| CREATE INDEX IF NOT EXISTS indexed_events_ledger_sequence_idx | ||
| ON indexed_events (ledger_sequence); |
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.
onRestart/onRescanschedule multiplesetTimeoutcallbacks but don’t clear them on unmount. If the user navigates away quickly, React can warn about state updates on an unmounted component. Consider tracking timeout IDs (e.g., in a ref) and clearing them in a cleanup function.