Skip to content

Commit 18a3f39

Browse files
authored
Add separate BTC and USD monitoring pages (#135)
1 parent 104d4bc commit 18a3f39

19 files changed

+1472
-4
lines changed

e2e/mock-server.js

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
const http = require('http');
2+
const fs = require('fs');
3+
const path = require('path');
4+
5+
const ASSETS = path.join(__dirname, '..', 'src', 'assets');
6+
7+
const MOCK_DATA = {
8+
channels: [
9+
{ name: 'peer-alice', capacity: 5000000, localBalance: 3000000, remoteBalance: 2000000 },
10+
],
11+
balances: [
12+
{
13+
assetName: 'Bitcoin',
14+
assetSymbol: 'BTC',
15+
onchainBalance: 35390000,
16+
lndOnchainBalance: 35300000,
17+
lightningBalance: 128180000,
18+
citreaBalance: 60010000,
19+
customerBalance: 161260000,
20+
ldsBalance: 97640000,
21+
ldsBalanceInCHF: 5000.0,
22+
assetPriceInCHF: 50000.0,
23+
},
24+
],
25+
evmBalances: [
26+
{
27+
blockchain: 'ethereum',
28+
nativeSymbol: 'ETH',
29+
nativeBalance: 0.5,
30+
tokens: [
31+
{ symbol: 'WBTC', address: '0xwbtc', balance: 0.0037 },
32+
{ symbol: 'USDC', address: '0xusdc', balance: 839.99 },
33+
{ symbol: 'USDT', address: '0xusdt', balance: 2630.35 },
34+
],
35+
},
36+
{
37+
blockchain: 'citrea',
38+
nativeSymbol: 'cBTC',
39+
nativeBalance: 0.6001,
40+
tokens: [
41+
{ symbol: 'WBTCe', address: '0xwbtce', balance: 0.0012 },
42+
{ symbol: 'JUSD', address: '0xjusd', balance: 57999.35 },
43+
],
44+
},
45+
{
46+
blockchain: 'polygon',
47+
nativeSymbol: 'MATIC',
48+
nativeBalance: 10.0,
49+
tokens: [
50+
{ symbol: 'USDT', address: '0xusdt-poly', balance: 40091.03 },
51+
],
52+
},
53+
],
54+
timestamp: new Date().toISOString(),
55+
};
56+
57+
function generateBtcHistory(range) {
58+
const now = Date.now();
59+
const intervals = { '24h': { count: 24, step: 60 * 60 * 1000 }, '7d': { count: 28, step: 6 * 60 * 60 * 1000 }, '30d': { count: 30, step: 24 * 60 * 60 * 1000 } };
60+
const cfg = intervals[range] || intervals['24h'];
61+
const points = [];
62+
for (let i = cfg.count; i >= 0; i--) {
63+
const ts = new Date(now - i * cfg.step).toISOString();
64+
const base = 0.95 + Math.sin(i * 0.3) * 0.05;
65+
points.push({ timestamp: ts, netBalance: parseFloat(base.toFixed(8)) });
66+
}
67+
return points;
68+
}
69+
70+
function generateUsdHistory(range) {
71+
const now = Date.now();
72+
const intervals = { '24h': { count: 24, step: 60 * 60 * 1000 }, '7d': { count: 28, step: 6 * 60 * 60 * 1000 }, '30d': { count: 30, step: 24 * 60 * 60 * 1000 } };
73+
const cfg = intervals[range] || intervals['24h'];
74+
const points = [];
75+
for (let i = cfg.count; i >= 0; i--) {
76+
const ts = new Date(now - i * cfg.step).toISOString();
77+
const base = 101000 + Math.sin(i * 0.4) * 2000;
78+
points.push({ timestamp: ts, totalBalance: parseFloat(base.toFixed(2)) });
79+
}
80+
return points;
81+
}
82+
83+
const server = http.createServer((req, res) => {
84+
const url = new URL(req.url, 'http://localhost:3099');
85+
86+
if (url.pathname === '/monitoring/btc/history') {
87+
const range = url.searchParams.get('range') || '24h';
88+
res.writeHead(200, { 'Content-Type': 'application/json' });
89+
res.end(JSON.stringify({ points: generateBtcHistory(range), range }));
90+
return;
91+
}
92+
93+
if (url.pathname === '/monitoring/usd/history') {
94+
const range = url.searchParams.get('range') || '24h';
95+
res.writeHead(200, { 'Content-Type': 'application/json' });
96+
res.end(JSON.stringify({ points: generateUsdHistory(range), range }));
97+
return;
98+
}
99+
100+
if (req.url === '/monitoring/data') {
101+
res.writeHead(200, { 'Content-Type': 'application/json' });
102+
res.end(JSON.stringify(MOCK_DATA));
103+
return;
104+
}
105+
106+
const routes = {
107+
'/monitoring': 'monitoring.html',
108+
'/monitoring/monitoring.js': 'monitoring.js',
109+
'/monitoring/btc': 'monitoring-btc.html',
110+
'/monitoring/btc.js': 'monitoring-btc.js',
111+
'/monitoring/usd': 'monitoring-usd.html',
112+
'/monitoring/usd.js': 'monitoring-usd.js',
113+
};
114+
115+
const file = routes[req.url];
116+
if (file) {
117+
const filePath = path.join(ASSETS, file);
118+
const contentType = file.endsWith('.js') ? 'application/javascript' : 'text/html';
119+
try {
120+
const content = fs.readFileSync(filePath, 'utf-8');
121+
res.writeHead(200, { 'Content-Type': contentType });
122+
res.end(content);
123+
} catch {
124+
res.writeHead(404);
125+
res.end('Not found');
126+
}
127+
return;
128+
}
129+
130+
res.writeHead(404);
131+
res.end('Not found');
132+
});
133+
134+
server.listen(3099, () => {
135+
console.log('Mock server running on http://localhost:3099');
136+
});

e2e/monitoring-btc-chart.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('BTC Monitoring Chart', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/monitoring/btc');
6+
});
7+
8+
test('displays chart section with canvas', async ({ page }) => {
9+
await expect(page.locator('h2').filter({ hasText: 'BTC Balance History' })).toBeVisible();
10+
await expect(page.locator('#btcChart')).toBeVisible();
11+
});
12+
13+
test('displays range buttons with 24h active by default', async ({ page }) => {
14+
const buttons = page.locator('.range-buttons button');
15+
await expect(buttons).toHaveCount(3);
16+
await expect(buttons.nth(0)).toHaveText('24h');
17+
await expect(buttons.nth(1)).toHaveText('7d');
18+
await expect(buttons.nth(2)).toHaveText('30d');
19+
20+
await expect(buttons.nth(0)).toHaveClass(/active/);
21+
await expect(buttons.nth(1)).not.toHaveClass(/active/);
22+
await expect(buttons.nth(2)).not.toHaveClass(/active/);
23+
});
24+
25+
test('switching range updates active button', async ({ page }) => {
26+
const buttons = page.locator('.range-buttons button');
27+
28+
await buttons.nth(1).click();
29+
await expect(buttons.nth(0)).not.toHaveClass(/active/);
30+
await expect(buttons.nth(1)).toHaveClass(/active/);
31+
32+
await buttons.nth(2).click();
33+
await expect(buttons.nth(1)).not.toHaveClass(/active/);
34+
await expect(buttons.nth(2)).toHaveClass(/active/);
35+
36+
await buttons.nth(0).click();
37+
await expect(buttons.nth(2)).not.toHaveClass(/active/);
38+
await expect(buttons.nth(0)).toHaveClass(/active/);
39+
});
40+
41+
test('chart fetches history data on load', async ({ page }) => {
42+
const [response] = await Promise.all([
43+
page.waitForResponse((r) => r.url().includes('/monitoring/btc/history')),
44+
page.goto('/monitoring/btc'),
45+
]);
46+
expect(response.status()).toBe(200);
47+
});
48+
49+
test('switching range fetches new data', async ({ page }) => {
50+
await page.waitForResponse((r) => r.url().includes('/monitoring/btc/history'));
51+
52+
const [response] = await Promise.all([
53+
page.waitForResponse((r) => r.url().includes('range=7d')),
54+
page.locator('.range-buttons button').nth(1).click(),
55+
]);
56+
expect(response.status()).toBe(200);
57+
const data = await response.json();
58+
expect(data.range).toBe('7d');
59+
});
60+
});

e2e/monitoring-btc.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('BTC Monitoring Page', () => {
4+
test.beforeEach(async ({ page }) => {
5+
await page.goto('/monitoring/btc');
6+
});
7+
8+
test('page loads with correct title', async ({ page }) => {
9+
await expect(page).toHaveTitle('lightning.space - BTC Monitoring');
10+
});
11+
12+
test('displays header and navigation', async ({ page }) => {
13+
await expect(page.locator('h1')).toHaveText('lightning.space - BTC Monitoring');
14+
await expect(page.locator('.nav')).toContainText('Overview');
15+
await expect(page.locator('.nav')).toContainText('USD Monitoring');
16+
await expect(page.locator('.nav a[href="/monitoring"]')).toBeVisible();
17+
await expect(page.locator('.nav a[href="/monitoring/usd"]')).toBeVisible();
18+
});
19+
20+
test('displays BTC Holdings Breakdown table', async ({ page }) => {
21+
await expect(page.locator('h2').filter({ hasText: 'BTC Holdings Breakdown' })).toBeVisible();
22+
23+
const table = page.locator('table').first();
24+
await expect(table).toBeVisible();
25+
26+
// Check header columns
27+
await expect(table.locator('th').nth(0)).toHaveText('Source');
28+
await expect(table.locator('th').nth(1)).toHaveText('Location');
29+
await expect(table.locator('th').nth(2)).toHaveText('BTC');
30+
31+
// Check all 6 source rows exist
32+
const rows = table.locator('tr').filter({ hasNot: page.locator('th') });
33+
await expect(rows).toHaveCount(7); // 6 sources + 1 total row
34+
35+
// Verify source names
36+
await expect(table).toContainText('Onchain BTC');
37+
await expect(table).toContainText('LND Onchain');
38+
await expect(table).toContainText('Lightning');
39+
await expect(table).toContainText('cBTC');
40+
await expect(table).toContainText('WBTC');
41+
await expect(table).toContainText('WBTCe');
42+
43+
// Verify locations
44+
await expect(table).toContainText('Bitcoin');
45+
await expect(table).toContainText('LND Wallet');
46+
await expect(table).toContainText('LN Channels');
47+
await expect(table).toContainText('Citrea');
48+
await expect(table).toContainText('Ethereum');
49+
});
50+
51+
test('correctly converts sats to BTC', async ({ page }) => {
52+
const table = page.locator('table').first();
53+
54+
// Onchain BTC: 35390000 sats = 0.35390000 BTC
55+
const onchainRow = table.locator('tr').filter({ hasText: 'Onchain BTC' });
56+
await expect(onchainRow.locator('td.number')).toHaveText('0.35390000');
57+
58+
// LND Onchain: 35300000 sats = 0.35300000 BTC
59+
const lndRow = table.locator('tr').filter({ hasText: 'LND Onchain' });
60+
await expect(lndRow.locator('td.number')).toHaveText('0.35300000');
61+
62+
// Lightning: 128180000 sats = 1.28180000 BTC
63+
const lnRow = table.locator('tr').filter({ hasText: 'Lightning' });
64+
await expect(lnRow.locator('td.number')).toHaveText('1.28180000');
65+
66+
// cBTC: 60010000 sats = 0.60010000 BTC
67+
const cbtcRow = table.locator('tr').filter({ hasText: /^cBTC/ });
68+
await expect(cbtcRow.locator('td.number')).toHaveText('0.60010000');
69+
});
70+
71+
test('correctly shows EVM token balances in BTC', async ({ page }) => {
72+
const table = page.locator('table').first();
73+
74+
// WBTC: 0.0037 BTC (already in BTC) - use td:first-child to match exactly
75+
const wbtcRow = table.locator('tr').filter({ has: page.locator('td:first-child', { hasText: /^WBTC$/ }) });
76+
await expect(wbtcRow.locator('td.number')).toHaveText('0.00370000');
77+
78+
// WBTCe: 0.0012 BTC
79+
const wbtceRow = table.locator('tr').filter({ has: page.locator('td:first-child', { hasText: /^WBTCe$/ }) });
80+
await expect(wbtceRow.locator('td.number')).toHaveText('0.00120000');
81+
});
82+
83+
test('displays correct total holdings', async ({ page }) => {
84+
const table = page.locator('table').first();
85+
const totalRow = table.locator('tr.total-row');
86+
await expect(totalRow).toContainText('Total');
87+
88+
// Total = 0.3539 + 0.353 + 1.2818 + 0.6001 + 0.0037 + 0.0012 = 2.5937
89+
await expect(totalRow.locator('td.number')).toHaveText('2.59370000');
90+
});
91+
92+
test('displays BTC Balance Sheet', async ({ page }) => {
93+
await expect(page.locator('h2').filter({ hasText: 'BTC Balance Sheet' })).toBeVisible();
94+
95+
const balanceTable = page.locator('table').nth(1);
96+
await expect(balanceTable).toBeVisible();
97+
98+
await expect(balanceTable).toContainText('Total Holdings');
99+
await expect(balanceTable).toContainText('Customer Balance');
100+
await expect(balanceTable).toContainText('LDS Net Position');
101+
});
102+
103+
test('shows correct customer balance as negative', async ({ page }) => {
104+
const balanceTable = page.locator('table').nth(1);
105+
const customerRow = balanceTable.locator('tr').filter({ hasText: 'Customer Balance' });
106+
// Customer: 161260000 sats = 1.6126 BTC, shown as -1.61260000
107+
await expect(customerRow.locator('td.number')).toHaveText('-1.61260000');
108+
await expect(customerRow.locator('td.number')).toHaveClass(/negative/);
109+
});
110+
111+
test('shows correct net position with color', async ({ page }) => {
112+
const balanceTable = page.locator('table').nth(1);
113+
const netRow = balanceTable.locator('tr.total-row');
114+
// Net = 2.5937 - 1.6126 = 0.9811
115+
await expect(netRow.locator('td.number')).toHaveText('0.98110000');
116+
await expect(netRow.locator('td.number')).toHaveClass(/positive/);
117+
});
118+
119+
test('shows timestamp after data loads', async ({ page }) => {
120+
const timestamp = page.locator('#timestamp');
121+
await expect(timestamp).not.toHaveText('Loading...');
122+
await expect(timestamp).toContainText('Last updated:');
123+
});
124+
});

e2e/monitoring-navigation.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Monitoring Navigation', () => {
4+
test('main monitoring page has links to BTC and USD pages', async ({ page }) => {
5+
await page.goto('/monitoring');
6+
await expect(page.locator('a[href="/monitoring/btc"]')).toBeVisible();
7+
await expect(page.locator('a[href="/monitoring/usd"]')).toBeVisible();
8+
});
9+
10+
test('BTC page links to overview and USD', async ({ page }) => {
11+
await page.goto('/monitoring/btc');
12+
await expect(page.locator('a[href="/monitoring"]')).toBeVisible();
13+
await expect(page.locator('a[href="/monitoring/usd"]')).toBeVisible();
14+
});
15+
16+
test('USD page links to overview and BTC', async ({ page }) => {
17+
await page.goto('/monitoring/usd');
18+
await expect(page.locator('a[href="/monitoring"]')).toBeVisible();
19+
await expect(page.locator('a[href="/monitoring/btc"]')).toBeVisible();
20+
});
21+
22+
test('can navigate from main to BTC page', async ({ page }) => {
23+
await page.goto('/monitoring');
24+
await page.click('a[href="/monitoring/btc"]');
25+
await expect(page).toHaveTitle('lightning.space - BTC Monitoring');
26+
});
27+
28+
test('can navigate from main to USD page', async ({ page }) => {
29+
await page.goto('/monitoring');
30+
await page.click('a[href="/monitoring/usd"]');
31+
await expect(page).toHaveTitle('lightning.space - USD Monitoring');
32+
});
33+
34+
test('can navigate between BTC and USD pages', async ({ page }) => {
35+
await page.goto('/monitoring/btc');
36+
await page.click('a[href="/monitoring/usd"]');
37+
await expect(page).toHaveTitle('lightning.space - USD Monitoring');
38+
39+
await page.click('a[href="/monitoring/btc"]');
40+
await expect(page).toHaveTitle('lightning.space - BTC Monitoring');
41+
});
42+
});

0 commit comments

Comments
 (0)