Files
copilot/src/entrypoints/popup/main.ts
T
selimaj-devandclaude 3b6054f2f0 Connect to the real Nexa backend, replacing the mock lock toggle
Adds the wire-protocol WebSocket client (ws-client.ts) and the REST
auth flow (backend-client.ts) against the now-live backend, using a
locally-generated ed25519 keypair (identity.ts) as a stand-in wallet
signer until real wallet-extension integration is built.

lock-state.ts's setLockState is now applyLockState, called only by
the WS client on an incoming lock_state message -- the backend is
the sole source of truth for lock state, so there's no other writer
anymore. The popup's dev mock controls are replaced with a live
connection-status + lock-state display.

Verified end-to-end against the real backend (nonce -> verify -> /me
-> ws lock_state) using the same tweetnacl/bs58 libs shipped in the
extension.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YXiHuScXrjxBh7yFGAPq3B
2026-09-07 09:10:18 +02:00

72 lines
2.5 KiB
TypeScript

import { browser } from 'wxt/browser';
import { DEFAULT_CONNECTION_STATUS, DEFAULT_LOCK_STATE, type ConnectionStatus, type LockState } from '@/shared/types';
import type { NexaMessage } from '@/shared/messaging';
const app = document.getElementById('app');
if (!app) throw new Error('Popup root element #app not found.');
app.innerHTML = `
<header>
<h1>Nexa</h1>
<p class="subtitle">Trading companion</p>
</header>
<section class="connection" id="connection"></section>
<section class="status" id="status"></section>
<a class="thresholds-link" id="thresholds-link" href="#" title="Threshold settings are not part of this milestone">
Adjust loss thresholds →
</a>
`;
const connectionEl = document.getElementById('connection') as HTMLElement;
const statusEl = document.getElementById('status') as HTMLElement;
function escapeHtml(value: string): string {
const div = document.createElement('div');
div.textContent = value;
return div.innerHTML;
}
const CONNECTION_LABELS: Record<ConnectionStatus, string> = {
connecting: 'Connecting…',
connected: 'Connected',
disconnected: 'Disconnected — retrying',
'auth-error': 'Sign-in failed — retrying',
};
function renderConnection(status: ConnectionStatus): void {
connectionEl.innerHTML = `<span class="connection-dot connection-${status}"></span> ${CONNECTION_LABELS[status]}`;
}
function renderStatus(state: LockState): void {
statusEl.innerHTML = state.locked
? `<div class="badge badge-locked">🔒 Locked</div>
<p class="reason">${escapeHtml(state.reason ?? 'No reason provided.')}</p>
<p class="scope">Scope: ${escapeHtml(state.scope)}</p>`
: `<div class="badge badge-unlocked">Unlocked</div>`;
}
async function loadLockState(): Promise<LockState> {
const message: NexaMessage = { type: 'nexa:get-lock-state' };
const state = (await browser.runtime.sendMessage(message)) as LockState | undefined;
return state ?? DEFAULT_LOCK_STATE;
}
async function loadConnectionStatus(): Promise<ConnectionStatus> {
const message: NexaMessage = { type: 'nexa:get-connection-status' };
const status = (await browser.runtime.sendMessage(message)) as ConnectionStatus | undefined;
return status ?? DEFAULT_CONNECTION_STATUS;
}
loadLockState().then(renderStatus);
loadConnectionStatus().then(renderConnection);
browser.runtime.onMessage.addListener((message: NexaMessage) => {
if (message?.type === 'nexa:lock-state-changed') {
renderStatus(message.state);
} else if (message?.type === 'nexa:connection-status-changed') {
renderConnection(message.status);
}
});