@antseed/dashboard 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/dist/api/routes.d.ts +84 -0
- package/dist/api/routes.d.ts.map +1 -0
- package/dist/api/routes.js +454 -0
- package/dist/api/routes.js.map +1 -0
- package/dist/api/routes.test.d.ts +2 -0
- package/dist/api/routes.test.d.ts.map +1 -0
- package/dist/api/routes.test.js +77 -0
- package/dist/api/routes.test.js.map +1 -0
- package/dist/api/websocket.d.ts +25 -0
- package/dist/api/websocket.d.ts.map +1 -0
- package/dist/api/websocket.js +51 -0
- package/dist/api/websocket.js.map +1 -0
- package/dist/config-io.d.ts +7 -0
- package/dist/config-io.d.ts.map +1 -0
- package/dist/config-io.js +22 -0
- package/dist/config-io.js.map +1 -0
- package/dist/dht-query-service.d.ts +52 -0
- package/dist/dht-query-service.d.ts.map +1 -0
- package/dist/dht-query-service.js +276 -0
- package/dist/dht-query-service.js.map +1 -0
- package/dist/dht-query-service.test.d.ts +2 -0
- package/dist/dht-query-service.test.d.ts.map +1 -0
- package/dist/dht-query-service.test.js +77 -0
- package/dist/dht-query-service.test.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +65 -0
- package/dist/server.js.map +1 -0
- package/dist/status.d.ts +9 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +97 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist-web/assets/index-CsvLvJ8Z.css +1 -0
- package/dist-web/assets/index-Da6V1FWz.js +142 -0
- package/dist-web/index.html +13 -0
- package/package.json +43 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { DashboardConfig, NodeStatus } from './types.js';
|
|
2
|
+
export { createDashboardServer } from './server.js';
|
|
3
|
+
export type { DashboardServer } from './server.js';
|
|
4
|
+
export { getNodeStatus } from './status.js';
|
|
5
|
+
export { broadcastEvent, getConnectedClientCount } from './api/websocket.js';
|
|
6
|
+
export type { WsEvent, WsEventType } from './api/websocket.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7E,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { DashboardConfig } from './types.js';
|
|
3
|
+
export interface DashboardServer {
|
|
4
|
+
start(): Promise<void>;
|
|
5
|
+
stop(): Promise<void>;
|
|
6
|
+
getInstance(): FastifyInstance;
|
|
7
|
+
}
|
|
8
|
+
export interface DashboardServerOptions {
|
|
9
|
+
configPath?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Create the dashboard Fastify server.
|
|
13
|
+
* Serves the built React app as static files and exposes API endpoints.
|
|
14
|
+
*
|
|
15
|
+
* @param config - The dashboard configuration (satisfies DashboardConfig)
|
|
16
|
+
* @param port - Port to listen on
|
|
17
|
+
* @returns DashboardServer instance
|
|
18
|
+
*/
|
|
19
|
+
export declare function createDashboardServer(config: DashboardConfig, port: number, options?: DashboardServerOptions): Promise<DashboardServer>;
|
|
20
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAMnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAQlD,MAAM,WAAW,eAAe;IAC9B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,WAAW,IAAI,eAAe,CAAC;CAChC;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,eAAe,EACvB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,eAAe,CAAC,CAqD1B"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import fastifyCors from '@fastify/cors';
|
|
3
|
+
import fastifyStatic from '@fastify/static';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { registerApiRoutes } from './api/routes.js';
|
|
8
|
+
import { registerWebSocket, broadcastEvent } from './api/websocket.js';
|
|
9
|
+
import { DHTQueryService } from './dht-query-service.js';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
/**
|
|
13
|
+
* Create the dashboard Fastify server.
|
|
14
|
+
* Serves the built React app as static files and exposes API endpoints.
|
|
15
|
+
*
|
|
16
|
+
* @param config - The dashboard configuration (satisfies DashboardConfig)
|
|
17
|
+
* @param port - Port to listen on
|
|
18
|
+
* @returns DashboardServer instance
|
|
19
|
+
*/
|
|
20
|
+
export async function createDashboardServer(config, port, options) {
|
|
21
|
+
const app = Fastify({ logger: false });
|
|
22
|
+
// Deny cross-origin requests
|
|
23
|
+
await app.register(fastifyCors, { origin: false });
|
|
24
|
+
// Create DHTQueryService for live network visibility
|
|
25
|
+
const dhtQueryService = new DHTQueryService(config);
|
|
26
|
+
// Serve built dashboard static files.
|
|
27
|
+
// __dirname resolves to antseed-dashboard/dist/ at runtime;
|
|
28
|
+
// the web app is built to antseed-dashboard/dist-web/.
|
|
29
|
+
const distPath = path.resolve(__dirname, '../dist-web');
|
|
30
|
+
if (!existsSync(path.join(distPath, 'index.html'))) {
|
|
31
|
+
console.warn(`Warning: dashboard UI not found at ${distPath}/index.html. Run "npm run build:web" in the dashboard package.`);
|
|
32
|
+
}
|
|
33
|
+
await app.register(fastifyStatic, {
|
|
34
|
+
root: distPath,
|
|
35
|
+
prefix: '/',
|
|
36
|
+
});
|
|
37
|
+
// Register API routes with DHT query service
|
|
38
|
+
await registerApiRoutes(app, config, dhtQueryService, options?.configPath);
|
|
39
|
+
// Register WebSocket handler
|
|
40
|
+
await registerWebSocket(app);
|
|
41
|
+
// Wire DHT peer updates to WebSocket broadcasts
|
|
42
|
+
dhtQueryService.onPeersUpdated((peers) => {
|
|
43
|
+
broadcastEvent({
|
|
44
|
+
type: 'network_peers_updated',
|
|
45
|
+
data: peers,
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
start: async () => {
|
|
51
|
+
await app.listen({ port, host: '127.0.0.1' });
|
|
52
|
+
// Start DHT query service after Fastify is listening
|
|
53
|
+
await dhtQueryService.start().catch((err) => {
|
|
54
|
+
console.error(`DHT start failed (non-fatal): ${err.message}`);
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
stop: async () => {
|
|
58
|
+
// Stop DHT query service before Fastify closes
|
|
59
|
+
await dhtQueryService.stop().catch(() => { });
|
|
60
|
+
await app.close();
|
|
61
|
+
},
|
|
62
|
+
getInstance: () => app,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,OAA4B,MAAM,SAAS,CAAC;AACnD,OAAO,WAAW,MAAM,eAAe,CAAC;AACxC,OAAO,aAAa,MAAM,iBAAiB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAY3C;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,MAAuB,EACvB,IAAY,EACZ,OAAgC;IAEhC,MAAM,GAAG,GAAG,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAEvC,6BAA6B;IAC7B,MAAM,GAAG,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;IAEnD,qDAAqD;IACrD,MAAM,eAAe,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC;IAEpD,sCAAsC;IACtC,4DAA4D;IAC5D,uDAAuD;IACvD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IAExD,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC;QACnD,OAAO,CAAC,IAAI,CAAC,sCAAsC,QAAQ,gEAAgE,CAAC,CAAC;IAC/H,CAAC;IAED,MAAM,GAAG,CAAC,QAAQ,CAAC,aAAa,EAAE;QAChC,IAAI,EAAE,QAAQ;QACd,MAAM,EAAE,GAAG;KACZ,CAAC,CAAC;IAEH,6CAA6C;IAC7C,MAAM,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;IAE3E,6BAA6B;IAC7B,MAAM,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAE7B,gDAAgD;IAChD,eAAe,CAAC,cAAc,CAAC,CAAC,KAAK,EAAE,EAAE;QACvC,cAAc,CAAC;YACb,IAAI,EAAE,uBAAuB;YAC7B,IAAI,EAAE,KAAK;YACX,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,MAAM,GAAG,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;YAC9C,qDAAqD;YACrD,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;gBACnD,OAAO,CAAC,KAAK,CAAC,iCAAkC,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;YAC3E,CAAC,CAAC,CAAC;QACL,CAAC;QACD,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,+CAA+C;YAC/C,MAAM,eAAe,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC7C,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC;QACD,WAAW,EAAE,GAAG,EAAE,CAAC,GAAG;KACvB,CAAC;AACJ,CAAC"}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DashboardConfig, NodeStatus } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Query the current node status.
|
|
4
|
+
* Reads from the running daemon's state file at ~/.antseed/daemon.state.json.
|
|
5
|
+
* Uses PID-based liveness check first, falls back to 30s stale threshold.
|
|
6
|
+
* Returns idle state with zeroed metrics if no daemon is running or the state file is stale.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getNodeStatus(config: DashboardConfig): Promise<NodeStatus>;
|
|
9
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAkC9D;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CA+ChF"}
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
/** Stale threshold: 30 seconds */
|
|
5
|
+
const STALE_THRESHOLD_MS = 30_000;
|
|
6
|
+
function asFiniteNumber(value, fallback) {
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
9
|
+
}
|
|
10
|
+
function asNullablePort(value) {
|
|
11
|
+
if (value === null || value === undefined || value === '') {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const parsed = Number(value);
|
|
15
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 65535) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check if a process with the given PID is alive.
|
|
22
|
+
* Uses process.kill(pid, 0) which checks existence without sending a signal.
|
|
23
|
+
*/
|
|
24
|
+
function isProcessAlive(pid) {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Query the current node status.
|
|
35
|
+
* Reads from the running daemon's state file at ~/.antseed/daemon.state.json.
|
|
36
|
+
* Uses PID-based liveness check first, falls back to 30s stale threshold.
|
|
37
|
+
* Returns idle state with zeroed metrics if no daemon is running or the state file is stale.
|
|
38
|
+
*/
|
|
39
|
+
export async function getNodeStatus(config) {
|
|
40
|
+
const stateFilePath = join(homedir(), '.antseed', 'daemon.state.json');
|
|
41
|
+
try {
|
|
42
|
+
const raw = await readFile(stateFilePath, 'utf-8');
|
|
43
|
+
const state = JSON.parse(raw);
|
|
44
|
+
// PID-based liveness check
|
|
45
|
+
const pid = typeof state['pid'] === 'number' ? state['pid'] : null;
|
|
46
|
+
const alive = pid !== null && isProcessAlive(pid);
|
|
47
|
+
// If PID is present and process is dead, return idle immediately
|
|
48
|
+
if (pid !== null && !alive) {
|
|
49
|
+
return idleStatus(config, pid);
|
|
50
|
+
}
|
|
51
|
+
// Fallback: if no PID in state file, use stale threshold
|
|
52
|
+
if (pid === null) {
|
|
53
|
+
const fileStat = await stat(stateFilePath);
|
|
54
|
+
const ageMs = Date.now() - fileStat.mtimeMs;
|
|
55
|
+
if (ageMs > STALE_THRESHOLD_MS) {
|
|
56
|
+
return idleStatus(config, null);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const validStates = ['seeding', 'connected', 'idle'];
|
|
60
|
+
const rawState = typeof state['state'] === 'string' && validStates.includes(state['state'])
|
|
61
|
+
? state['state']
|
|
62
|
+
: 'idle';
|
|
63
|
+
return {
|
|
64
|
+
state: rawState,
|
|
65
|
+
peerCount: asFiniteNumber(state['peerCount'], 0),
|
|
66
|
+
earningsToday: typeof state['earningsToday'] === 'string' ? state['earningsToday'] : '0',
|
|
67
|
+
tokensToday: asFiniteNumber(state['tokensToday'], 0),
|
|
68
|
+
activeSessions: asFiniteNumber(state['activeSessions'], 0),
|
|
69
|
+
uptime: typeof state['uptime'] === 'string' ? state['uptime'] : '0s',
|
|
70
|
+
walletAddress: typeof state['walletAddress'] === 'string' ? state['walletAddress'] : (config.identity.walletAddress ?? null),
|
|
71
|
+
proxyPort: asNullablePort(state['proxyPort']),
|
|
72
|
+
capacityUsedPercent: asFiniteNumber(state['capacityUsedPercent'], 0),
|
|
73
|
+
daemonPid: pid,
|
|
74
|
+
daemonAlive: alive,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// State file doesn't exist or is unreadable — daemon is not running
|
|
79
|
+
return idleStatus(config, null);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function idleStatus(config, pid) {
|
|
83
|
+
return {
|
|
84
|
+
state: 'idle',
|
|
85
|
+
peerCount: 0,
|
|
86
|
+
earningsToday: '0',
|
|
87
|
+
tokensToday: 0,
|
|
88
|
+
activeSessions: 0,
|
|
89
|
+
uptime: '0s',
|
|
90
|
+
walletAddress: config.identity.walletAddress ?? null,
|
|
91
|
+
proxyPort: null,
|
|
92
|
+
capacityUsedPercent: 0,
|
|
93
|
+
daemonPid: pid,
|
|
94
|
+
daemonAlive: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration subset required by the dashboard.
|
|
3
|
+
* Matches the shape of AntseedConfig from antseed-cli,
|
|
4
|
+
* but is defined independently so the dashboard package has no
|
|
5
|
+
* compile-time dependency on the CLI internals.
|
|
6
|
+
*
|
|
7
|
+
* The CLI passes its full AntseedConfig object — which satisfies
|
|
8
|
+
* this interface — when calling createDashboardServer().
|
|
9
|
+
*/
|
|
10
|
+
export interface DashboardConfig {
|
|
11
|
+
identity: {
|
|
12
|
+
displayName: string;
|
|
13
|
+
walletAddress?: string;
|
|
14
|
+
};
|
|
15
|
+
seller: {
|
|
16
|
+
enabledProviders: string[];
|
|
17
|
+
reserveFloor: number;
|
|
18
|
+
maxConcurrentBuyers: number;
|
|
19
|
+
pricing: HierarchicalPricingConfig;
|
|
20
|
+
};
|
|
21
|
+
buyer: {
|
|
22
|
+
preferredProviders: string[];
|
|
23
|
+
maxPricing: HierarchicalPricingConfig;
|
|
24
|
+
minPeerReputation: number;
|
|
25
|
+
proxyPort: number;
|
|
26
|
+
};
|
|
27
|
+
network: {
|
|
28
|
+
bootstrapNodes: string[];
|
|
29
|
+
};
|
|
30
|
+
payments: {
|
|
31
|
+
preferredMethod: string;
|
|
32
|
+
platformFeeRate: number;
|
|
33
|
+
crypto?: {
|
|
34
|
+
chainId: string;
|
|
35
|
+
rpcUrl: string;
|
|
36
|
+
escrowContractAddress: string;
|
|
37
|
+
usdcContractAddress: string;
|
|
38
|
+
defaultLockAmountUSDC?: string;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
providers?: unknown[];
|
|
42
|
+
plugins?: {
|
|
43
|
+
name: string;
|
|
44
|
+
package: string;
|
|
45
|
+
installedAt: string;
|
|
46
|
+
}[];
|
|
47
|
+
}
|
|
48
|
+
export interface TokenPricingUsdPerMillion {
|
|
49
|
+
inputUsdPerMillion: number;
|
|
50
|
+
outputUsdPerMillion: number;
|
|
51
|
+
}
|
|
52
|
+
export interface ProviderPricingConfig {
|
|
53
|
+
defaults?: TokenPricingUsdPerMillion;
|
|
54
|
+
models?: Record<string, TokenPricingUsdPerMillion>;
|
|
55
|
+
}
|
|
56
|
+
export interface HierarchicalPricingConfig {
|
|
57
|
+
defaults: TokenPricingUsdPerMillion;
|
|
58
|
+
providers?: Record<string, ProviderPricingConfig>;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Current node status returned by getNodeStatus().
|
|
62
|
+
*/
|
|
63
|
+
export interface NodeStatus {
|
|
64
|
+
state: 'seeding' | 'connected' | 'idle';
|
|
65
|
+
peerCount: number;
|
|
66
|
+
earningsToday: string;
|
|
67
|
+
tokensToday: number;
|
|
68
|
+
activeSessions: number;
|
|
69
|
+
uptime: string;
|
|
70
|
+
walletAddress: string | null;
|
|
71
|
+
proxyPort: number | null;
|
|
72
|
+
capacityUsedPercent: number;
|
|
73
|
+
daemonPid: number | null;
|
|
74
|
+
daemonAlive: boolean;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE;QACR,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,MAAM,EAAE;QACN,gBAAgB,EAAE,MAAM,EAAE,CAAC;QAC3B,YAAY,EAAE,MAAM,CAAC;QACrB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,OAAO,EAAE,yBAAyB,CAAC;KACpC,CAAC;IACF,KAAK,EAAE;QACL,kBAAkB,EAAE,MAAM,EAAE,CAAC;QAC7B,UAAU,EAAE,yBAAyB,CAAC;QACtC,iBAAiB,EAAE,MAAM,CAAC;QAC1B,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,OAAO,EAAE;QACP,cAAc,EAAE,MAAM,EAAE,CAAC;KAC1B,CAAC;IACF,QAAQ,EAAE;QACR,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;QACxB,MAAM,CAAC,EAAE;YACP,OAAO,EAAE,MAAM,CAAC;YAChB,MAAM,EAAE,MAAM,CAAC;YACf,qBAAqB,EAAE,MAAM,CAAC;YAC9B,mBAAmB,EAAE,MAAM,CAAC;YAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;SAChC,CAAC;KACH,CAAC;IACF,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC;IACtB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACpE;AAED,MAAM,WAAW,yBAAyB;IACxC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,yBAAyB,CAAC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,SAAS,GAAG,WAAW,GAAG,MAAM,CAAC;IACxC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;CACtB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg-primary: #0b0e17;--bg-secondary: #111827;--bg-surface: #1e293b;--bg-hover: #1e293b;--text-primary: #f1f5f9;--text-secondary: #94a3b8;--text-muted: #64748b;--accent: #f43f5e;--accent-green: #10b981;--accent-yellow: #f59e0b;--accent-blue: #3b82f6;--border: #1e293b;--border-light: #334155;--sidebar-width: 220px;--radius: 8px;--shadow: 0 1px 3px rgba(0,0,0,.3), 0 1px 2px rgba(0,0,0,.2)}*{margin:0;padding:0;box-sizing:border-box}body{font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;background:var(--bg-primary);color:var(--text-primary);font-size:13px;-webkit-font-smoothing:antialiased}.app-container{display:flex;height:100vh}.sidebar{width:var(--sidebar-width);background:var(--bg-secondary);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0}.sidebar-header{padding:20px 16px 12px;border-bottom:1px solid var(--border)}.sidebar-logo{display:flex;align-items:center;gap:10px}.sidebar-title{font-size:18px;font-weight:700;background:linear-gradient(135deg,var(--accent),#fb7185);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.sidebar-version{font-size:10px;color:var(--text-muted);margin-top:4px;letter-spacing:.5px}.sidebar-nav{list-style:none;padding:12px 8px;flex:1}.sidebar-btn{width:100%;padding:10px 12px;background:none;border:none;color:var(--text-secondary);cursor:pointer;display:flex;align-items:center;gap:10px;font-size:13px;text-align:left;border-radius:6px;transition:all .15s ease}.sidebar-btn:hover{background:var(--bg-hover);color:var(--text-primary)}.sidebar-btn.active{background:#f43f5e1a;color:var(--accent);font-weight:600}.sidebar-btn.active svg{stroke:var(--accent)}.sidebar-footer{padding:16px;border-top:1px solid var(--border)}.sidebar-footer-text{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px}.main-content{flex:1;overflow-y:auto;padding:24px 28px;background:var(--bg-primary)}.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header h2{font-size:20px;font-weight:600;color:var(--text-primary)}.connection-badge{display:flex;align-items:center;gap:8px;padding:6px 14px;border-radius:20px;font-size:11px;font-weight:600;letter-spacing:.5px}.badge-active{background:#10b9811f;color:var(--accent-green)}.badge-idle{background:#94a3b81a;color:var(--text-secondary)}.dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.dot-active{background:var(--accent-green);box-shadow:0 0 8px #10b98180;animation:pulse 2s ease-in-out infinite}.dot-idle{background:var(--text-muted)}.dot-sm{width:6px;height:6px;border-radius:50%;flex-shrink:0}.dot-peer{background:var(--accent-blue)}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:20px}.stat-card{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;transition:border-color .15s ease}.stat-card:hover{border-color:var(--border-light)}.stat-card-header{display:flex;justify-content:space-between;align-items:center}.stat-label{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;font-weight:500}.stat-value{font-size:24px;font-weight:700;margin-top:6px;line-height:1.1}.stat-sub{font-size:11px;color:var(--text-muted);margin-top:4px}.stat-icon{color:var(--text-muted)}.loading{color:var(--text-secondary);padding:60px;text-align:center;display:flex;flex-direction:column;align-items:center;gap:12px}.loading-spinner{width:24px;height:24px;border:2px solid var(--border-light);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.overview-detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.overview-panel{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:18px}.panel-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px}.panel-header h3{font-size:12px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px}.panel-count{background:var(--bg-surface);color:var(--text-secondary);font-size:11px;padding:2px 8px;border-radius:10px;font-weight:600}.panel-divider{height:1px;background:var(--border);margin:16px 0}.capacity-section{display:flex;align-items:center;gap:24px}.capacity-ring{position:relative;width:100px;height:100px;flex-shrink:0}.capacity-ring-label{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}.capacity-ring-value{font-size:18px;font-weight:700;line-height:1}.capacity-ring-text{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-top:2px}.capacity-meta{flex:1}.capacity-row{display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px}.capacity-row:last-child{border-bottom:none}.capacity-label{color:var(--text-muted)}.capacity-val{color:var(--text-primary);font-weight:500}.mini-chart{padding:4px 0}.mini-chart-bars{display:flex;align-items:flex-end;gap:3px;height:64px}.mini-chart-bar-group{flex:1;display:flex;align-items:flex-end;justify-content:center;height:100%}.mini-chart-bar{background:linear-gradient(to top,var(--accent-green),rgba(16,185,129,.4));border-radius:2px 2px 0 0;min-width:4px;transition:height .3s ease}.mini-chart-bar-group:hover .mini-chart-bar{background:var(--accent-green)}.mini-chart-labels{display:flex;justify-content:space-between;margin-top:6px;font-size:10px;color:var(--text-muted)}.mini-chart-empty{color:var(--text-muted);font-size:12px;text-align:center;padding:20px 0}.peer-list-header{display:grid;grid-template-columns:2fr 1fr .5fr;gap:8px;padding:6px 0 8px;border-bottom:1px solid var(--border);font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}.peer-row{display:grid;grid-template-columns:2fr 1fr .5fr;gap:8px;padding:8px 0;border-bottom:1px solid var(--border);font-size:12px;align-items:center;transition:background .1s}.peer-row:last-child{border-bottom:none}.peer-row:hover{background:#ffffff05;margin:0 -18px;padding-left:18px;padding-right:18px}.peer-self{background:#f43f5e0a;margin:0 -18px;padding-left:18px;padding-right:18px;border-radius:4px}.peer-row-id{display:flex;align-items:center;gap:8px}.peer-badge-you{font-size:9px;font-weight:700;color:var(--accent);background:#f43f5e26;padding:1px 5px;border-radius:3px;letter-spacing:.5px}.peer-row-providers{display:flex;gap:4px;flex-wrap:wrap}.provider-chip{font-size:10px;padding:2px 6px;border-radius:3px;font-weight:500}.provider-anthropic{background:#f43f5e26;color:var(--accent)}.provider-openai{background:#10b98126;color:var(--accent-green)}.provider-google{background:#3b82f626;color:var(--accent-blue)}.peer-row-rep{color:var(--text-secondary);text-align:right}.empty-state{display:flex;flex-direction:column;align-items:center;padding:32px 0;gap:8px}.empty-icon{opacity:.4}.empty-text{font-size:13px;color:var(--text-secondary)}.empty-hint{font-size:11px;color:var(--text-muted)}.table-container{overflow-x:auto}.data-table{width:100%;border-collapse:collapse;font-size:12px}.data-table th{background:var(--bg-surface);padding:10px 14px;text-align:left;font-weight:600;color:var(--text-muted);border-bottom:1px solid var(--border);cursor:pointer;-webkit-user-select:none;user-select:none;white-space:nowrap;font-size:11px;text-transform:uppercase;letter-spacing:.3px}.data-table th.sorted{color:var(--accent)}.data-table td{padding:8px 14px;border-bottom:1px solid var(--border)}.data-table tbody tr{transition:background .1s}.data-table tbody tr:hover{background:#ffffff05}.mono{font-family:SF Mono,JetBrains Mono,Consolas,monospace;font-size:11px}.filter-input{background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);padding:8px 14px;font-size:12px;width:220px;transition:border-color .15s}.filter-input:focus{outline:none;border-color:var(--accent)}.rep-high{color:var(--accent-green);font-weight:500}.rep-mid{color:var(--accent-yellow);font-weight:500}.rep-low{color:var(--accent);font-weight:500}.earnings-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-bottom:24px}.chart-section{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:18px;margin-bottom:16px}.chart-section h3{font-size:12px;font-weight:600;color:var(--text-secondary);margin-bottom:14px;text-transform:uppercase;letter-spacing:.3px}.period-toggle{display:flex;gap:4px}.toggle-btn{padding:6px 14px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:6px;color:var(--text-secondary);cursor:pointer;font-size:12px;transition:all .15s}.toggle-btn:hover{border-color:var(--border-light);color:var(--text-primary)}.toggle-btn.active{background:#f43f5e1a;color:var(--accent);border-color:#f43f5e4d}.settings-section{background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:16px}.settings-section h3{font-size:12px;font-weight:600;color:var(--text-secondary);margin-bottom:16px;text-transform:uppercase;letter-spacing:.5px}.form-label{display:flex;flex-direction:column;gap:6px;margin-bottom:14px;font-size:12px;color:var(--text-secondary)}.form-input{background:var(--bg-primary);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);padding:8px 14px;font-size:13px;width:100%;max-width:320px;transition:border-color .15s}.form-input:focus{outline:none;border-color:var(--accent)}.save-btn{padding:8px 24px;background:var(--accent);border:none;border-radius:6px;color:#fff;font-size:12px;cursor:pointer;font-weight:600;transition:opacity .15s}.save-btn:hover{opacity:.9}.save-btn:disabled{opacity:.4;cursor:not-allowed}.settings-message{padding:10px 14px;margin-bottom:14px;background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;font-size:12px;color:var(--accent-green)}.source-badge{font-size:9px;font-weight:700;padding:1px 5px;border-radius:3px;letter-spacing:.5px}.source-badge-daemon{color:var(--accent-green);background:#10b98126}.source-badge-dht{color:var(--accent-blue);background:#3b82f626}.page-header-actions{display:flex;align-items:center;gap:8px}.scan-btn{padding:8px 16px;background:var(--bg-surface);border:1px solid var(--border);border-radius:6px;color:var(--text-secondary);cursor:pointer;font-size:12px;font-weight:500;transition:all .15s;white-space:nowrap}.scan-btn:hover{border-color:var(--accent-blue);color:var(--accent-blue)}.scan-btn:disabled{opacity:.5;cursor:not-allowed}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-light);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
|