Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 79 additions & 0 deletions src/app/transparency/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react"

const ADMIN_EVENTS = [
{
id: "1",
timestamp: "2024-06-25T14:30:00Z",
action: "Set Platform Fee",
details: "Fee updated to 2.5%",
caller: "GBXYZ...ABCD",
txHash: "9a8b7c6d5e4f3a2b1c0d...",
},
{
id: "2",
timestamp: "2024-06-20T09:15:00Z",
action: "Pause Contract",
details: "Emergency pause activated",
caller: "GBXYZ...ABCD",
txHash: "1a2b3c4d5e6f7a8b9c0d...",
},
{
id: "3",
timestamp: "2024-06-21T10:00:00Z",
action: "Unpause Contract",
details: "Operations resumed",
caller: "GBXYZ...ABCD",
txHash: "fedcba0987654321...",
},
]
Comment on lines +3 to +28

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Replace the hardcoded audit log with a real event source.

This page claims to be the public record of contract admin actions, but ADMIN_EVENTS is just static sample data. Shipping it as-is will show fictitious/stale caller addresses and tx hashes, which defeats the transparency feature.

Also applies to: 35-36

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/transparency/page.tsx` around lines 3 - 28, The transparency page is
still using the hardcoded ADMIN_EVENTS sample array, so it will display stale or
fictitious admin actions instead of a real public record. Replace ADMIN_EVENTS
in the page component with data fetched from the actual event source (or a
server-side data loader) and render that source dynamically in the existing
admin-events UI. Make sure the page uses real caller addresses, tx hashes,
timestamps, and action details rather than static placeholders.


export default function TransparencyPage() {
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Transparency Audit Log</h1>
<p className="text-slate-400">
Public record of all admin actions and protocol parameter changes on the SkillSphere smart contracts.
</p>
</div>

<div className="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="bg-slate-800 text-slate-300 border-b border-slate-700">
<tr>
<th className="px-6 py-4 font-semibold">Timestamp</th>
<th className="px-6 py-4 font-semibold">Action</th>
<th className="px-6 py-4 font-semibold">Caller</th>
<th className="px-6 py-4 font-semibold">Transaction Hash</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700">
{ADMIN_EVENTS.map((event) => (
<tr key={event.id} className="hover:bg-slate-800/30 transition-colors">
<td className="px-6 py-4 text-slate-400 whitespace-nowrap">
{new Date(event.timestamp).toLocaleString()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Format timestamps with an explicit timezone.

toLocaleString() here depends on the server/runtime locale and omits the timezone, so the same on-chain event can be shown differently across environments. For an audit surface, render a fixed format (for example UTC) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/transparency/page.tsx` at line 55, The timestamp rendering in the
transparency page currently uses Date.prototype.toLocaleString, which varies by
runtime locale and omits timezone context. Update the event timestamp display in
the transparency page component to use a fixed, explicit timezone format such as
UTC so the same event renders consistently across environments, and keep the
change localized to the JSX that formats event.timestamp.

</td>
<td className="px-6 py-4">
<div className="font-medium text-white">{event.action}</div>
<div className="text-xs text-slate-500 mt-1">{event.details}</div>
</td>
<td className="px-6 py-4">
<span className="font-mono text-purple-400 bg-purple-400/10 px-2 py-1 rounded">
{event.caller}
</span>
</td>
<td className="px-6 py-4">
<span className="font-mono text-slate-300">
{event.txHash}
</span>
</td>
</tr>
))}
Comment on lines +52 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Render the audit entries in a deterministic time order.

The current list is already out of sequence (2024-06-20 is rendered after 2024-06-25, then 2024-06-21). Audit logs need an explicit newest-first or oldest-first sort before mapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/transparency/page.tsx` around lines 52 - 72, The ADMIN_EVENTS
rendering in the transparency page is not ordered consistently, so audit entries
can appear out of sequence. Before mapping in the table render, sort the
ADMIN_EVENTS data explicitly by timestamp in a single deterministic order
(newest-first or oldest-first) using the existing page component logic around
the ADMIN_EVENTS.map block. Ensure the sorted collection is what gets rendered,
not the original unsorted array.

</tbody>
</table>
</div>
</div>
</div>
)
}
197 changes: 31 additions & 166 deletions src/components/dashboard/ExpertStats.tsx
Original file line number Diff line number Diff line change
@@ -1,174 +1,39 @@
"use client";
"use client"
import { useState } from "react"

import { useState, useEffect } from "react";
import { Activity, AlertCircle } from "lucide-react";
import { Card } from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import { useHeartbeatPing } from "@/hooks/useHeartbeat";
import { formatTimeAgo } from "@/utils/time";
export default function ExpertStats() {
const [isAvailable, setIsAvailable] = useState(false)

interface ExpertStatsProps {
expertId: string;
initialLastHeartbeat?: number | null;
}

export default function ExpertStats({
expertId,
initialLastHeartbeat,
}: ExpertStatsProps) {
const { isPinging, pingError, lastHeartbeat, handlePingNow, setLastHeartbeat } =
useHeartbeatPing(expertId);

// Initialize with provided heartbeat, update when hook provides new value
useEffect(() => {
if (initialLastHeartbeat !== undefined) {
setLastHeartbeat(initialLastHeartbeat ?? null);
}
}, [initialLastHeartbeat, setLastHeartbeat]);

// STEP E: Re-render every 30 seconds to keep "X mins ago" current
const [, forceUpdate] = useState({});
useEffect(() => {
const interval = setInterval(() => forceUpdate({}), 30000);
return () => clearInterval(interval);
}, []);

// Determine if heartbeat is considered "fresh" (within 1 hour = 3600 seconds)
const HEARTBEAT_VALIDITY_WINDOW = 3600; // 1 hour in seconds
const isHeartbeatFresh =
lastHeartbeat &&
Math.floor((Date.now() - lastHeartbeat) / 1000) < HEARTBEAT_VALIDITY_WINDOW;
const handleToggle = () => {
// Simulated on-chain heartbeat update
setIsAvailable(!isAvailable)
}
Comment on lines +5 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Persist the availability state instead of keeping it component-local.

Line 5 always reinitializes the toggle to false, so the status resets after a refresh/remount and misses the “persistent dashboard widget” requirement. Store and hydrate this flag from stable client storage (for example the same local-storage pattern already used in the dashboard header).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/ExpertStats.tsx` around lines 5 - 10, The
availability toggle in ExpertStats is currently only kept in local component
state, so it resets on remount and does not meet the persistent widget
requirement. Update the ExpertStats component to hydrate and save the flag from
stable client storage using the same local-storage approach already used
elsewhere in the dashboard, and make handleToggle update both the stored value
and the React state so the status survives refreshes.


return (
<div className="space-y-6">
{/* Heartbeat Status Panel */}
<Card className="p-6 bg-gradient-to-br from-emerald-500/10 to-emerald-600/5 border-emerald-500/20 hover:border-emerald-500/40 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-3">
<Activity className="w-5 h-5 text-emerald-400" />
<h3 className="text-lg font-semibold text-white">Availability Heartbeat</h3>
</div>

{/* Heartbeat Status Display */}
<div className="space-y-3">
<div className="bg-white/5 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-slate-400">Last Heartbeat</span>
{isHeartbeatFresh && (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full bg-emerald-500/20 border border-emerald-500/30">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
<span className="text-xs font-medium text-emerald-400">Online</span>
</span>
)}
</div>
<p className="text-2xl font-bold text-white">
{formatTimeAgo(lastHeartbeat)}
</p>
{!isHeartbeatFresh && lastHeartbeat && (
<p className="text-xs text-yellow-400 mt-2">
⚠️ Not visible in search. Send a heartbeat to stay online.
</p>
)}
{!lastHeartbeat && (
<p className="text-xs text-slate-400 mt-2">
Never sent a heartbeat. Send one to become visible in search.
</p>
)}
</div>

{/* Heartbeat Information */}
<div className="bg-white/[0.02] rounded-lg p-3 border border-white/5">
<p className="text-xs text-slate-400 leading-relaxed">
Send a heartbeat to keep your profile active and visible to seekers.
Your heartbeat status is valid for <strong>1 hour</strong> after sending.
Experts without a recent heartbeat cannot accept new sessions.
</p>
</div>
</div>
</div>

{/* Ping Button */}
<div className="ml-4 flex-shrink-0">
<Button
onClick={handlePingNow}
disabled={isPinging}
className="bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-400 border border-emerald-500/30 disabled:opacity-50 disabled:cursor-not-allowed"
>
<Activity className="w-4 h-4 mr-2" />
{isPinging ? "Pinging..." : "Ping Now"}
</Button>
</div>
</div>

{/* Error State */}
{pingError && (
<div className="mt-4 flex items-start gap-3 p-3 rounded-lg bg-red-500/10 border border-red-500/20">
<AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-400" role="alert">
{pingError}
</p>
</div>
)}
</Card>

{/* Online Status Quick Info */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Status Info Card */}
<Card
className={`p-4 border transition-colors ${
isHeartbeatFresh
? "bg-emerald-500/5 border-emerald-500/20"
: "bg-slate-500/5 border-slate-500/20"
<div className="flex items-center gap-3 bg-slate-800/50 px-4 py-2 rounded-full border border-slate-700">
<div className="flex items-center gap-2">
<div
className={`w-2.5 h-2.5 rounded-full ${
isAvailable ? "bg-green-500" : "bg-gray-500"
}`}
>
<div className="flex items-center gap-3">
<div
className={`w-3 h-3 rounded-full animate-pulse ${
isHeartbeatFresh ? "bg-emerald-400" : "bg-slate-400"
}`}
/>
<div>
<p className="text-xs text-slate-400">Current Status</p>
<p className={`font-semibold ${
isHeartbeatFresh ? "text-emerald-400" : "text-slate-400"
}`}>
{isHeartbeatFresh ? "Online & Visible" : "Offline"}
</p>
</div>
</div>
</Card>

{/* Validity Window Info Card */}
<Card className="p-4 bg-blue-500/5 border border-blue-500/20">
<div className="flex items-center gap-3">
<div className="w-3 h-3 rounded-full bg-blue-400" />
<div>
<p className="text-xs text-slate-400">Validity Window</p>
<p className="font-semibold text-blue-400">1 hour from send</p>
</div>
</div>
</Card>
/>
<span className="text-sm font-medium text-slate-200">
{isAvailable ? "Available Now" : "Offline"}
</span>
</div>

{/* Recommended Action */}
{!isHeartbeatFresh && (
<div className="bg-gradient-to-r from-purple-500/10 to-pink-500/10 rounded-lg p-4 border border-purple-500/20">
<h4 className="font-semibold text-white mb-2">📌 Stay Visible</h4>
<p className="text-sm text-slate-300 mb-3">
Send a heartbeat regularly to maintain your visibility in the search index.
Consider setting up automatic heartbeats to avoid going offline.
</p>
<Button
onClick={handlePingNow}
disabled={isPinging}
className="bg-purple-600 hover:bg-purple-700 text-white"
size="sm"
>
{isPinging ? "Pinging..." : "Send Heartbeat Now"}
</Button>
</div>
)}
<button
onClick={handleToggle}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
isAvailable ? "bg-green-500/30" : "bg-slate-700"
}`}
aria-label="Toggle availability"
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
isAvailable ? "translate-x-6" : "translate-x-1"
}`}
/>
Comment on lines +24 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose the current toggle state to assistive tech.

This is implemented as a switch, but the button only announces “Toggle availability” and not whether it is currently on or off. Add switch/toggle state semantics such as role="switch" with aria-checked={isAvailable} (or aria-pressed).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/dashboard/ExpertStats.tsx` around lines 24 - 35, The
availability toggle in ExpertStats should expose its current state to assistive
tech, since it is currently only labeled and not announced as on or off. Update
the button in the toggle UI to use proper switch semantics in the component that
renders handleToggle/isAvailable, such as adding role="switch" with aria-checked
bound to isAvailable (or equivalent pressed state), while keeping the existing
aria-label and visual behavior unchanged.

</button>
</div>
);
)
}
3 changes: 3 additions & 0 deletions src/components/dashboard/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { useEffect, useState } from "react"
import { ChevronDown } from "lucide-react"
import { safeLocalStorage } from "@/utils/safeLocalStorage"
import ExpertStats from "./ExpertStats"

const profiles = [
{ id: "nora", name: "Miss Nora" },
{ id: "sam", name: "Mr Sam" },
Expand Down Expand Up @@ -70,6 +72,7 @@ export default function Header({
</div>

<div className="flex items-center gap-4">
<ExpertStats />


<div>
Expand Down
45 changes: 39 additions & 6 deletions src/components/marketplace/FundSessionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,46 @@ export default function FundSessionModal({
const [currentStep, setCurrentStep] = useState<Step>('duration');
const [duration, setDuration] = useState<number>(60); // minutes
const [isProcessing, setIsProcessing] = useState(false);
const [rentFee, setRentFee] = useState<number | null>(null);
const [isEstimatingRent, setIsEstimatingRent] = useState(false);

const hourlyRate = parseInt(expertHourlyRate?.replace(/\D/g, '') || '50');
const amount = (hourlyRate * duration) / 60;
const sessionId = `SESSION_${Date.now()}`;

// Simulate RPC call for state rent
const fetchRentEstimate = async () => {
setIsEstimatingRent(true);
try {
// Dummy RPC dry-run simulation
await new Promise((resolve) => setTimeout(resolve, 800));
setRentFee(0.015); // Mock rent fee in XLM
} catch (e) {
console.error("Failed to estimate rent fee", e);
setRentFee(0.01); // Fallback mock fee
Comment on lines +45 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Treat rent-estimation failures as blocking, not as 0.01 XLM.

If the dry-run fails here, totalRequired and the insufficient-balance check still use this made-up fee as if it came from RPC. That can understate the funds required and let an underfunded checkout continue with the wrong total. Keep the estimate unavailable, surface an error, and block confirmation until a real estimate succeeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/marketplace/FundSessionModal.tsx` around lines 45 - 47, The
rent estimation fallback in FundSessionModal should not use a fake 0.01 XLM
value when the dry-run fails, because that lets totalRequired and the balance
check proceed with an incorrect fee. Update the rent-estimation flow in the
relevant estimate/rent-fee logic so failures leave the estimate unavailable,
surface an error state, and prevent confirmation until a real RPC-backed fee is
obtained. Use the existing setRentFee and the surrounding checkout/confirmation
logic in FundSessionModal to block the action instead of continuing with a
placeholder.

} finally {
setIsEstimatingRent(false);
}
};

React.useEffect(() => {
if (isOpen) {
fetchRentEstimate();
}
}, [isOpen, duration]);
Comment on lines +31 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Track the latest rent-estimate request before re-enabling checkout.

Lines 38-57 can overlap when the user changes duration quickly. An older fetchRentEstimate() completion always runs setIsEstimatingRent(false), so cannotProceed can flip back to false while a newer estimate is still pending. The first open also renders once with rentFee === null and isEstimatingRent === false before the effect fires, briefly showing a 0 XLM total and an enabled Continue button. Tie the loading/result state to a request id (or cancel stale requests) and mark estimation pending synchronously when isOpen/duration changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/marketplace/FundSessionModal.tsx` around lines 31 - 57, The
rent estimation flow in FundSessionModal can be overwritten by stale async
completions, causing isEstimatingRent and rentFee to reflect an older request
and briefly enabling checkout with a 0 XLM total. Update fetchRentEstimate and
the React.useEffect trigger so each duration/isOpen change creates a fresh
request identity (or cancels prior requests) and only the latest request is
allowed to clear loading or set rentFee. Also set the estimating state
synchronously before kicking off the request so the initial render and rapid
duration changes never show a false-ready Continue state.


const walletBalance = balance !== null ? parseFloat(balance) : null;
const totalRequired = amount + (rentFee || 0) + MIN_XLM_FOR_FEES;

const isBalanceUnavailable =
isLoading ||
error !== null ||
!address ||
walletBalance === null ||
Number.isNaN(walletBalance);
const hasInsufficientBalance =
!isBalanceUnavailable && walletBalance < amount + MIN_XLM_FOR_FEES;
const cannotProceed = isBalanceUnavailable || hasInsufficientBalance;
!isBalanceUnavailable && walletBalance < totalRequired;
const cannotProceed = isBalanceUnavailable || hasInsufficientBalance || isEstimatingRent;

const handleDurationChange = (value: number) => {
setDuration(value);
Expand Down Expand Up @@ -145,9 +170,13 @@ export default function FundSessionModal({
<span className="text-muted-foreground">Duration</span>
<span>{duration} minutes</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Estimated State Rent Fee</span>
<span>{isEstimatingRent ? '...' : rentFee ? `${rentFee.toFixed(3)} XLM` : '0 XLM'}</span>
</div>
<div className="border-t border-purple-500/20 pt-3 flex justify-between font-bold">
<span>Total Amount</span>
<span className="text-purple-400">${amount.toFixed(2)} XLM</span>
<span className="text-purple-400">${(amount + (rentFee || 0)).toFixed(3)} XLM</span>
Comment on lines +173 to +179

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a single currency label for the XLM totals.

These rows currently render values like $50.015 XLM, which mixes fiat and XLM in the same amount. Show either 50.015 XLM or convert to a real fiat value before using $.

Also applies to: 209-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/marketplace/FundSessionModal.tsx` around lines 173 - 179, The
XLM amount rows in FundSessionModal are mixing fiat and token labels by
prefixing totals with "$" while still rendering "XLM"; update the rendering in
the estimated rent and total amount sections so they use a single consistent
currency label. Locate the amount display logic in FundSessionModal and either
remove the dollar sign for XLM-denominated values or convert the value to fiat
before keeping "$", and apply the same fix to the other affected amount block
mentioned in the comment.

</div>
</div>

Expand All @@ -159,7 +188,7 @@ export default function FundSessionModal({
disabled={cannotProceed}
className="w-full px-4 py-3 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 disabled:from-purple-600/50 disabled:to-pink-600/50 rounded-lg font-semibold transition-all flex items-center justify-center gap-2 disabled:cursor-not-allowed"
>
Continue
{isEstimatingRent ? 'Estimating Rent...' : 'Continue'}
<ChevronRight size={20} />
</button>
</div>
Expand All @@ -177,9 +206,13 @@ export default function FundSessionModal({
<span className="text-muted-foreground">Duration</span>
<span className="font-semibold">{duration} minutes</span>
</div>
<div className="flex justify-between items-center">
<span className="text-gray-400">Estimated State Rent Fee</span>
<span className="font-semibold">{rentFee ? `${rentFee.toFixed(3)} XLM` : '0 XLM'}</span>
</div>
<div className="flex justify-between items-center pb-4 border-b border-purple-500/20">
<span className="text-muted-foreground">Total Amount</span>
<span className="font-semibold text-lg text-purple-400">${amount.toFixed(2)} XLM</span>
<span className="text-gray-400">Total Amount</span>
<span className="font-semibold text-lg text-purple-400">${(amount + (rentFee || 0)).toFixed(3)} XLM</span>
</div>
</div>

Expand Down
Loading