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
72 lines
2.5 KiB
TypeScript
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);
|
|
}
|
|
});
|