Skip to content

Commit f4bf55a

Browse files
author
Ope Olatunji
committed
fix: archive only closed positions, add filters/pagination/detail modal
Archive logic: - Only archives trades for CLOSED positions (failed, rejected, cancelled, no_wallet) - Filled trades for positions still held on-chain stay in Trade History - Filled trades only move to archive when position is sold or market resolved - Stops emptying the Trade History tab Archive GET endpoint: - Added pagination (page, pageSize params) - Added filters (side, status, search) - Returns totalPages for pagination UI Archive display: - Uses renderFilteredTable with search, filters (Side, Outcome, Status), sort by date - Clicking a row opens the standard detail modal (same as live trading) - Shows P&L and Cost columns - Proper empty state message explaining when trades get archived
1 parent 5cb450d commit f4bf55a

File tree

3 files changed

+51
-30
lines changed

3 files changed

+51
-30
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agenticmail/enterprise",
3-
"version": "0.5.532",
3+
"version": "0.5.533",
44
"description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
55
"type": "module",
66
"bin": {

src/admin/routes.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4286,29 +4286,39 @@ export function createAdminRoutes(db: DatabaseAdapter) {
42864286

42874287
// ── Archive System ──────────────────────────────────────────
42884288

4289-
// Get archived data for a specific tab
4289+
// Get archived data for a specific tab (with pagination and filters)
42904290
api.get('/polymarket/:agentId/archive/:tab', requireRole('admin'), async (c) => {
42914291
try {
42924292
const agentId = c.req.param('agentId');
42934293
const tab = c.req.param('tab');
42944294
const e = edb();
4295-
const limit = parseInt(c.req.query('limit') || '100');
4295+
const page = parseInt(c.req.query('page') || '1');
4296+
const pageSize = parseInt(c.req.query('pageSize') || '20');
4297+
const side = c.req.query('side') || '';
4298+
const status = c.req.query('status') || '';
4299+
const search = c.req.query('search') || '';
4300+
const offset = (page - 1) * pageSize;
42964301

42974302
let tableName = '';
42984303
if (tab === 'trades') tableName = 'poly_trade_log_archive';
42994304
else if (tab === 'exits') tableName = 'poly_exit_rules_archive';
43004305
else if (tab === 'alerts') tableName = 'poly_price_alerts_archive';
43014306
else if (tab === 'events') tableName = 'poly_watcher_events_archive';
4302-
else return c.json({ rows: [], total: 0, message: 'Unknown archive tab: ' + tab });
4307+
else return c.json({ rows: [], total: 0, page, pageSize, message: 'Unknown archive tab: ' + tab });
43034308

4304-
// Check if archive table exists
43054309
try {
4306-
const rows = await e?.all(`SELECT * FROM ${tableName} WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?`, [agentId, limit]) || [];
4307-
const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE agent_id = ?`, [agentId]) as any;
4308-
return c.json({ rows, total: countRow?.cnt || rows.length });
4310+
let where = `agent_id = ?`;
4311+
const params: any[] = [agentId];
4312+
if (side) { where += ` AND side = ?`; params.push(side.toUpperCase()); }
4313+
if (status) { where += ` AND status = ?`; params.push(status); }
4314+
if (search) { where += ` AND (market_question LIKE ? OR outcome LIKE ?)`; params.push(`%${search}%`, `%${search}%`); }
4315+
4316+
const countRow = await e?.get(`SELECT COUNT(*) as cnt FROM ${tableName} WHERE ${where}`, params) as any;
4317+
const total = countRow?.cnt || 0;
4318+
const rows = await e?.all(`SELECT * FROM ${tableName} WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, pageSize, offset]) || [];
4319+
return c.json({ rows, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });
43094320
} catch {
4310-
// Archive table doesn't exist yet — no archived data
4311-
return c.json({ rows: [], total: 0, message: 'No archived data yet. Use the Archive action to move old data here.' });
4321+
return c.json({ rows: [], total: 0, page, pageSize, totalPages: 0, message: 'No archived data yet.' });
43124322
}
43134323
} catch (e: any) { return c.json({ rows: [], total: 0, error: e.message }); }
43144324
});
@@ -4331,13 +4341,16 @@ export function createAdminRoutes(db: DatabaseAdapter) {
43314341
}
43324342

43334343
if (tab === 'trades' || tab === 'all') {
4334-
// Archive non-active trades (filled, failed, rejected, cancelled, no_position, no_wallet)
4344+
// Only archive trades that are truly closed:
4345+
// 1. Failed, rejected, cancelled, no_position, no_wallet — these never executed
4346+
// 2. Filled trades ONLY if the position is no longer held on-chain (sold or market resolved)
4347+
// Do NOT archive filled trades for positions we still hold!
43354348
await e?.run(`
4336-
INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log
4337-
WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')
4349+
INSERT INTO poly_trade_log_archive SELECT * FROM poly_trade_log
4350+
WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')
43384351
`, [agentId]).catch(() => {});
43394352
await e?.run(`
4340-
DELETE FROM poly_trade_log WHERE agent_id = $1 AND status NOT IN ('placed', 'pending')
4353+
DELETE FROM poly_trade_log WHERE agent_id = $1 AND status IN ('failed', 'rejected', 'cancelled', 'no_position', 'no_wallet')
43414354
`, [agentId]).catch(() => {});
43424355

43434356
// Also archive 'placed' trades for positions that no longer exist on-chain

src/dashboard/pages/polymarket.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2024,22 +2024,30 @@ export function PolymarketPage() {
20242024
showArchive.trades ? (
20252025
archiveLoading ? h('div', { style: { textAlign: 'center', padding: 40, color: 'var(--text-muted)' } }, 'Loading archive...') :
20262026
h('div', null,
2027-
h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 12, color: 'var(--text-muted)' } }, I('database'), ' Archived Trades (', (showArchive.trades_data?.total || 0), ')'),
2028-
(showArchive.trades_data?.rows || []).length === 0 ? h('div', { style: { textAlign: 'center', padding: 40, color: 'var(--text-muted)' } }, 'No archived trades yet.') :
2029-
h('div', { className: 'table-container' }, h('table', { className: 'data-table' },
2030-
h('thead', null, h('tr', null, ['Market','Side','Outcome','Shares','Price','Status','Date'].map(function(hd) { return h('th', { key: hd }, hd); }))),
2031-
h('tbody', null, (showArchive.trades_data?.rows || []).map(function(t) {
2032-
return h('tr', { key: t.id },
2033-
h('td', null, h('div', { style: { maxWidth: 220 } }, t.market_question || '')),
2034-
h('td', null, sideBadge(t.side)),
2035-
h('td', null, t.outcome ? h('span', { className: 'badge badge-' + (t.outcome.toLowerCase() === 'yes' ? 'success' : 'danger') }, t.outcome) : '--'),
2036-
h('td', null, (t.size || 0).toFixed(1)),
2037-
h('td', null, ((t.price || 0) * 100).toFixed(1) + '\u00a2'),
2038-
h('td', null, h('span', { className: 'badge badge-' + (t.status === 'filled' ? 'success' : t.status === 'failed' ? 'danger' : 'secondary') }, t.status)),
2039-
h('td', null, fmtDate(t.created_at))
2040-
);
2041-
}))
2042-
))
2027+
h('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 12, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 8 } },
2028+
I('database'), ' Archived Trades (', (showArchive.trades_data?.total || 0), ')',
2029+
showArchive.trades_data?.message && h('span', { style: { fontWeight: 400, fontStyle: 'italic' } }, ' — ', showArchive.trades_data.message)
2030+
),
2031+
renderFilteredTable('archive', showArchive.trades_data?.rows || [], 'No archived trades yet. Only trades for closed positions (sold or resolved markets) are archived.',
2032+
['Market', 'Side', 'Outcome', 'Shares', 'Price', 'Cost', 'Status', 'P&L', 'Date'],
2033+
function(t) { return [
2034+
h('td', null, h('div', { style: { maxWidth: "220px" } }, t.market_question || shortId(t.token_id))),
2035+
h('td', null, sideBadge(t.side)),
2036+
h('td', null, t.outcome
2037+
? h('span', { className: 'badge ' + (t.outcome.toLowerCase() === 'yes' ? 'badge-success' : t.outcome.toLowerCase() === 'no' ? 'badge-danger' : 'badge-secondary') }, t.outcome)
2038+
: '--'),
2039+
h('td', null, (t.size || 0).toFixed(1)),
2040+
h('td', null, ((t.fill_price || t.price || 0) * 100).toFixed(1) + '\u00a2'),
2041+
h('td', null, '$' + ((t.fill_price || t.price || 0) * (t.size || 0)).toFixed(2)),
2042+
h('td', null, h('span', { className: 'badge badge-' + (t.status === 'filled' ? 'success' : t.status === 'failed' || t.status === 'no_wallet' ? 'danger' : 'secondary') }, t.status)),
2043+
h('td', null, t.pnl != null ? pnlCell(t.pnl) : '--'),
2044+
h('td', null, fmtDate(t.created_at)),
2045+
]; },
2046+
{ searchFields: ['market_question', 'token_id', 'outcome'], filters: [
2047+
{ key: 'side', label: 'Side', options: ['BUY', 'SELL'] },
2048+
{ key: 'outcome', label: 'Outcome', options: ['Yes', 'No'] },
2049+
{ key: 'status', label: 'Status', options: ['filled', 'failed', 'cancelled', 'rejected', 'no_wallet'] }
2050+
], sortFn: function(a, b) { return new Date(b.created_at || 0).getTime() - new Date(a.created_at || 0).getTime(); } })
20432051
)
20442052
) :
20452053
renderFilteredTable('history', tradeHistory, 'No trade history',

0 commit comments

Comments
 (0)