|
|
@@ -2,6 +2,10 @@
|
|
|
|
|
|
import { useState } from 'react'
|
|
|
import useSWR from 'swr'
|
|
|
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
|
+import { Badge } from '@/components/ui/badge'
|
|
|
+import { Button } from '@/components/ui/button'
|
|
|
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
|
|
|
|
const fetcher = (url: string) => fetch(url).then((r) => r.json())
|
|
|
|
|
|
@@ -49,47 +53,50 @@ function StatusCard() {
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
|
|
|
- <div className="flex items-center justify-between mb-3">
|
|
|
- <h3 className="text-sm font-medium">Monitor Status</h3>
|
|
|
- <span
|
|
|
- className={`text-xs px-2 py-0.5 rounded-full ${
|
|
|
- status?.running ? 'bg-green-500/20 text-green-400' : 'bg-zinc-500/20 text-zinc-400'
|
|
|
- }`}
|
|
|
- >
|
|
|
- {status?.running ? 'Running' : 'Stopped'}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <p className="text-xs text-zinc-500 mb-3">Watching {status?.watchedCount || 0} addresses</p>
|
|
|
- {error && (
|
|
|
- <p className="text-xs text-red-400 mb-2 bg-red-500/10 px-2 py-1 rounded">{error}</p>
|
|
|
- )}
|
|
|
- {status?.errors?.length > 0 && (
|
|
|
- <div className="mb-3 max-h-20 overflow-y-auto space-y-1">
|
|
|
- {status.errors.map((err: string, i: number) => (
|
|
|
- <p key={i} className="text-[10px] text-red-400/80 bg-red-500/5 px-2 py-0.5 rounded truncate" title={err}>
|
|
|
- {err}
|
|
|
- </p>
|
|
|
- ))}
|
|
|
+ <Card>
|
|
|
+ <CardContent className="p-4">
|
|
|
+ <div className="flex items-center justify-between mb-3">
|
|
|
+ <h3 className="text-sm font-medium">Monitor Status</h3>
|
|
|
+ {status?.running ? (
|
|
|
+ <Badge variant="success">Running</Badge>
|
|
|
+ ) : (
|
|
|
+ <Badge variant="secondary">Stopped</Badge>
|
|
|
+ )}
|
|
|
</div>
|
|
|
- )}
|
|
|
- <div className="flex gap-2">
|
|
|
- <button
|
|
|
- onClick={handleStart}
|
|
|
- disabled={status?.running || loading}
|
|
|
- className="px-3 py-1.5 text-xs rounded bg-indigo-500 text-white hover:bg-indigo-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
|
- >
|
|
|
- {loading && !status?.running ? 'Starting...' : 'Start'}
|
|
|
- </button>
|
|
|
- <button
|
|
|
- onClick={handleStop}
|
|
|
- disabled={!status?.running || loading}
|
|
|
- className="px-3 py-1.5 text-xs rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
|
- >
|
|
|
- {loading && status?.running ? 'Stopping...' : 'Stop'}
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <p className="text-xs text-muted-foreground mb-3">Watching {status?.watchedCount || 0} addresses</p>
|
|
|
+ {error && (
|
|
|
+ <p className="text-xs text-red-400 mb-2 bg-red-500/10 px-2 py-1 rounded">{error}</p>
|
|
|
+ )}
|
|
|
+ {status?.errors?.length > 0 && (
|
|
|
+ <div className="mb-3 max-h-20 overflow-y-auto space-y-1">
|
|
|
+ {status.errors.map((err: string, i: number) => (
|
|
|
+ <p key={i} className="text-[10px] text-red-400/80 bg-red-500/5 px-2 py-0.5 rounded truncate" title={err}>
|
|
|
+ {err}
|
|
|
+ </p>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div className="flex gap-2">
|
|
|
+ <Button
|
|
|
+ onClick={handleStart}
|
|
|
+ disabled={status?.running || loading}
|
|
|
+ size="sm"
|
|
|
+ className="text-xs"
|
|
|
+ >
|
|
|
+ {loading && !status?.running ? 'Starting...' : 'Start'}
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ onClick={handleStop}
|
|
|
+ disabled={!status?.running || loading}
|
|
|
+ variant="destructive"
|
|
|
+ size="sm"
|
|
|
+ className="text-xs"
|
|
|
+ >
|
|
|
+ {loading && status?.running ? 'Stopping...' : 'Stop'}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
@@ -97,25 +104,27 @@ function WalletBalances() {
|
|
|
const { data } = useSWR('/api/wallet/balance', fetcher, { refreshInterval: 15000 })
|
|
|
|
|
|
return (
|
|
|
- <div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
|
|
|
- <h3 className="text-sm font-medium mb-3">Wallet Balances</h3>
|
|
|
- {data?.error ? (
|
|
|
- <p className="text-xs text-zinc-500">Configure SOL_SECRET_KEY to view balances</p>
|
|
|
- ) : (
|
|
|
- <div className="max-h-32 overflow-y-auto space-y-1.5">
|
|
|
- <div className="flex justify-between text-xs">
|
|
|
- <span className="text-zinc-500">SOL</span>
|
|
|
- <span>{data?.sol?.toFixed(4) || '...'}</span>
|
|
|
- </div>
|
|
|
- {data?.tokens?.map((t: { mint: string; symbol: string; amount: number }) => (
|
|
|
- <div key={t.mint} className="flex justify-between text-xs">
|
|
|
- <span className="text-zinc-500">{t.symbol}</span>
|
|
|
- <span>{t.amount.toFixed(4)}</span>
|
|
|
+ <Card>
|
|
|
+ <CardContent className="p-4">
|
|
|
+ <h3 className="text-sm font-medium mb-3">Wallet Balances</h3>
|
|
|
+ {data?.error ? (
|
|
|
+ <p className="text-xs text-muted-foreground">Configure SOL_SECRET_KEY to view balances</p>
|
|
|
+ ) : (
|
|
|
+ <div className="max-h-32 overflow-y-auto space-y-1.5">
|
|
|
+ <div className="flex justify-between text-xs">
|
|
|
+ <span className="text-muted-foreground">SOL</span>
|
|
|
+ <span>{data?.sol?.toFixed(4) || '...'}</span>
|
|
|
</div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
+ {data?.tokens?.map((t: { mint: string; symbol: string; amount: number }) => (
|
|
|
+ <div key={t.mint} className="flex justify-between text-xs">
|
|
|
+ <span className="text-muted-foreground">{t.symbol}</span>
|
|
|
+ <span>{t.amount.toFixed(4)}</span>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
@@ -181,85 +190,87 @@ function ActivePositions() {
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
|
|
|
- <div className="flex items-center justify-between mb-3">
|
|
|
- <h3 className="text-sm font-medium">Active Positions</h3>
|
|
|
- <span className="text-xs text-zinc-500">{rows.length} open</span>
|
|
|
- </div>
|
|
|
- {rows.length === 0 ? (
|
|
|
- <p className="text-xs text-zinc-500">No active positions</p>
|
|
|
- ) : (
|
|
|
- <div className="space-y-2 max-h-64 overflow-y-auto">
|
|
|
- {rows.map((row) => (
|
|
|
- <div
|
|
|
- key={row.id}
|
|
|
- className="flex items-center justify-between gap-3 rounded-md bg-zinc-800/40 px-3 py-2"
|
|
|
- >
|
|
|
- <div className="min-w-0 flex-1 space-y-0.5">
|
|
|
- <div className="flex items-center gap-2 text-xs">
|
|
|
- {row.pool_label ? (
|
|
|
- <span className="font-medium text-zinc-200">{row.pool_label}</span>
|
|
|
- ) : (
|
|
|
- <span className="font-mono text-zinc-300">
|
|
|
- {row.pool_id.slice(0, 6)}...{row.pool_id.slice(-4)}
|
|
|
- </span>
|
|
|
- )}
|
|
|
- {row.price_lower && row.price_upper ? (
|
|
|
- <span className="text-[10px] text-zinc-500">
|
|
|
- {formatPrice(row.price_lower)} ~ {formatPrice(row.price_upper)}
|
|
|
- </span>
|
|
|
- ) : (
|
|
|
- <span className="text-[10px] text-zinc-500">
|
|
|
- [{row.tick_lower} ~ {row.tick_upper}]
|
|
|
- </span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
- <div className="flex items-center gap-2 text-[10px] text-zinc-500">
|
|
|
- <span>
|
|
|
- Target:{' '}
|
|
|
- <a
|
|
|
- href={`https://solscan.io/account/${row.target_address}`}
|
|
|
- target="_blank"
|
|
|
- rel="noopener noreferrer"
|
|
|
- className="text-indigo-400/70 hover:underline"
|
|
|
- >
|
|
|
- {row.target_address.slice(0, 4)}...{row.target_address.slice(-4)}
|
|
|
- </a>
|
|
|
- </span>
|
|
|
- {row.our_nft_mint && (
|
|
|
+ <Card>
|
|
|
+ <CardContent className="p-4">
|
|
|
+ <div className="flex items-center justify-between mb-3">
|
|
|
+ <h3 className="text-sm font-medium">Active Positions</h3>
|
|
|
+ <span className="text-xs text-muted-foreground">{rows.length} open</span>
|
|
|
+ </div>
|
|
|
+ {rows.length === 0 ? (
|
|
|
+ <p className="text-xs text-muted-foreground">No active positions</p>
|
|
|
+ ) : (
|
|
|
+ <div className="space-y-2 max-h-64 overflow-y-auto">
|
|
|
+ {rows.map((row) => (
|
|
|
+ <div
|
|
|
+ key={row.id}
|
|
|
+ className="flex items-center justify-between gap-3 rounded-md bg-muted px-3 py-2"
|
|
|
+ >
|
|
|
+ <div className="min-w-0 flex-1 space-y-0.5">
|
|
|
+ <div className="flex items-center gap-2 text-xs">
|
|
|
+ {row.pool_label ? (
|
|
|
+ <span className="font-medium text-foreground">{row.pool_label}</span>
|
|
|
+ ) : (
|
|
|
+ <span className="font-mono text-foreground">
|
|
|
+ {row.pool_id.slice(0, 6)}...{row.pool_id.slice(-4)}
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ {row.price_lower && row.price_upper ? (
|
|
|
+ <span className="text-[10px] text-muted-foreground">
|
|
|
+ {formatPrice(row.price_lower)} ~ {formatPrice(row.price_upper)}
|
|
|
+ </span>
|
|
|
+ ) : (
|
|
|
+ <span className="text-[10px] text-muted-foreground">
|
|
|
+ [{row.tick_lower} ~ {row.tick_upper}]
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2 text-[10px] text-muted-foreground">
|
|
|
<span>
|
|
|
- NFT:{' '}
|
|
|
+ Target:{' '}
|
|
|
<a
|
|
|
- href={`https://solscan.io/token/${row.our_nft_mint}`}
|
|
|
+ href={`https://solscan.io/account/${row.target_address}`}
|
|
|
target="_blank"
|
|
|
rel="noopener noreferrer"
|
|
|
- className="text-indigo-400/70 hover:underline"
|
|
|
+ className="text-primary/70 hover:underline"
|
|
|
>
|
|
|
- {row.our_nft_mint.slice(0, 6)}...
|
|
|
+ {row.target_address.slice(0, 4)}...{row.target_address.slice(-4)}
|
|
|
</a>
|
|
|
</span>
|
|
|
+ {row.our_nft_mint && (
|
|
|
+ <span>
|
|
|
+ NFT:{' '}
|
|
|
+ <a
|
|
|
+ href={`https://solscan.io/token/${row.our_nft_mint}`}
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ className="text-primary/70 hover:underline"
|
|
|
+ >
|
|
|
+ {row.our_nft_mint.slice(0, 6)}...
|
|
|
+ </a>
|
|
|
+ </span>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div className="flex items-center gap-2 shrink-0">
|
|
|
+ {row.our_nft_mint && (
|
|
|
+ <Button
|
|
|
+ onClick={() => handleClose(row)}
|
|
|
+ disabled={closingId === row.id}
|
|
|
+ variant="outline"
|
|
|
+ size="sm"
|
|
|
+ className="h-6 text-[10px]"
|
|
|
+ >
|
|
|
+ {closingId === row.id ? 'Closing...' : 'Close'}
|
|
|
+ </Button>
|
|
|
)}
|
|
|
+ <Badge variant="success">active</Badge>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div className="flex items-center gap-2 shrink-0">
|
|
|
- {row.our_nft_mint && (
|
|
|
- <button
|
|
|
- onClick={() => handleClose(row)}
|
|
|
- disabled={closingId === row.id}
|
|
|
- className="px-2 py-0.5 text-[10px] rounded bg-orange-500/20 text-orange-400 hover:bg-orange-500/30 disabled:opacity-50 transition-colors"
|
|
|
- >
|
|
|
- {closingId === row.id ? 'Closing...' : 'Close'}
|
|
|
- </button>
|
|
|
- )}
|
|
|
- <span className="px-1.5 py-0.5 rounded text-[10px] bg-green-500/20 text-green-400">
|
|
|
- active
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
)
|
|
|
}
|
|
|
|
|
|
@@ -269,77 +280,79 @@ function RecentCopies() {
|
|
|
const rows = data || []
|
|
|
|
|
|
return (
|
|
|
- <div className="rounded-lg border border-[var(--border)] bg-[var(--card)] p-4">
|
|
|
- <h3 className="text-sm font-medium mb-3">Recent Copy Operations</h3>
|
|
|
- {rows.length === 0 ? (
|
|
|
- <p className="text-xs text-zinc-500">No copy operations yet</p>
|
|
|
- ) : (
|
|
|
- <div className="overflow-x-auto">
|
|
|
- <table className="w-full text-xs">
|
|
|
- <thead>
|
|
|
- <tr className="text-zinc-500 border-b border-[var(--border)]">
|
|
|
- <th className="text-left py-2 pr-3">Time</th>
|
|
|
- <th className="text-left py-2 pr-3">Operation</th>
|
|
|
- <th className="text-left py-2 pr-3">Target</th>
|
|
|
- <th className="text-left py-2 pr-3">Status</th>
|
|
|
- <th className="text-left py-2">TX</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- {rows.map(
|
|
|
- (row: {
|
|
|
- id: number
|
|
|
- created_at: string
|
|
|
- operation: string
|
|
|
- target_address: string
|
|
|
- status: string
|
|
|
- our_tx_sig: string | null
|
|
|
- }) => (
|
|
|
- <tr key={row.id} className="border-b border-zinc-800/50">
|
|
|
- <td className="py-2 pr-3 text-zinc-500">
|
|
|
- {new Date(row.created_at + 'Z').toLocaleTimeString()}
|
|
|
- </td>
|
|
|
- <td className="py-2 pr-3">{row.operation}</td>
|
|
|
- <td className="py-2 pr-3 text-zinc-500">
|
|
|
- {row.target_address.slice(0, 4)}...{row.target_address.slice(-4)}
|
|
|
- </td>
|
|
|
- <td className="py-2 pr-3">
|
|
|
- <span
|
|
|
- className={`px-1.5 py-0.5 rounded text-[10px] ${
|
|
|
- row.status === 'success'
|
|
|
- ? 'bg-green-500/20 text-green-400'
|
|
|
- : row.status === 'failed'
|
|
|
- ? 'bg-red-500/20 text-red-400'
|
|
|
- : row.status === 'executing'
|
|
|
- ? 'bg-yellow-500/20 text-yellow-400'
|
|
|
- : 'bg-zinc-500/20 text-zinc-400'
|
|
|
- }`}
|
|
|
- >
|
|
|
- {row.status}
|
|
|
- </span>
|
|
|
- </td>
|
|
|
- <td className="py-2">
|
|
|
- {row.our_tx_sig ? (
|
|
|
- <a
|
|
|
- href={`https://solscan.io/tx/${row.our_tx_sig}`}
|
|
|
- target="_blank"
|
|
|
- rel="noopener noreferrer"
|
|
|
- className="text-indigo-400 hover:underline"
|
|
|
+ <Card>
|
|
|
+ <CardContent className="p-4">
|
|
|
+ <h3 className="text-sm font-medium mb-3">Recent Copy Operations</h3>
|
|
|
+ {rows.length === 0 ? (
|
|
|
+ <p className="text-xs text-muted-foreground">No copy operations yet</p>
|
|
|
+ ) : (
|
|
|
+ <div className="overflow-x-auto">
|
|
|
+ <Table className="text-xs">
|
|
|
+ <TableHeader>
|
|
|
+ <TableRow>
|
|
|
+ <TableHead className="py-2 pr-3">Time</TableHead>
|
|
|
+ <TableHead className="py-2 pr-3">Operation</TableHead>
|
|
|
+ <TableHead className="py-2 pr-3">Target</TableHead>
|
|
|
+ <TableHead className="py-2 pr-3">Status</TableHead>
|
|
|
+ <TableHead className="py-2">TX</TableHead>
|
|
|
+ </TableRow>
|
|
|
+ </TableHeader>
|
|
|
+ <TableBody>
|
|
|
+ {rows.map(
|
|
|
+ (row: {
|
|
|
+ id: number
|
|
|
+ created_at: string
|
|
|
+ operation: string
|
|
|
+ target_address: string
|
|
|
+ status: string
|
|
|
+ our_tx_sig: string | null
|
|
|
+ }) => (
|
|
|
+ <TableRow key={row.id}>
|
|
|
+ <TableCell className="py-2 pr-3 text-muted-foreground">
|
|
|
+ {new Date(row.created_at + 'Z').toLocaleTimeString()}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="py-2 pr-3">{row.operation}</TableCell>
|
|
|
+ <TableCell className="py-2 pr-3 text-muted-foreground">
|
|
|
+ {row.target_address.slice(0, 4)}...{row.target_address.slice(-4)}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="py-2 pr-3">
|
|
|
+ <Badge
|
|
|
+ variant={
|
|
|
+ row.status === 'success'
|
|
|
+ ? 'success'
|
|
|
+ : row.status === 'failed'
|
|
|
+ ? 'destructive'
|
|
|
+ : row.status === 'executing'
|
|
|
+ ? 'warning'
|
|
|
+ : 'secondary'
|
|
|
+ }
|
|
|
>
|
|
|
- {row.our_tx_sig.slice(0, 8)}...
|
|
|
- </a>
|
|
|
- ) : (
|
|
|
- <span className="text-zinc-500">-</span>
|
|
|
- )}
|
|
|
- </td>
|
|
|
- </tr>
|
|
|
- ),
|
|
|
- )}
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- </div>
|
|
|
- )}
|
|
|
- </div>
|
|
|
+ {row.status}
|
|
|
+ </Badge>
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="py-2">
|
|
|
+ {row.our_tx_sig ? (
|
|
|
+ <a
|
|
|
+ href={`https://solscan.io/tx/${row.our_tx_sig}`}
|
|
|
+ target="_blank"
|
|
|
+ rel="noopener noreferrer"
|
|
|
+ className="text-primary hover:underline"
|
|
|
+ >
|
|
|
+ {row.our_tx_sig.slice(0, 8)}...
|
|
|
+ </a>
|
|
|
+ ) : (
|
|
|
+ <span className="text-muted-foreground">-</span>
|
|
|
+ )}
|
|
|
+ </TableCell>
|
|
|
+ </TableRow>
|
|
|
+ ),
|
|
|
+ )}
|
|
|
+ </TableBody>
|
|
|
+ </Table>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
)
|
|
|
}
|
|
|
|