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
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { browser } from 'wxt/browser';
|
||||
import { DEFAULT_LOCK_STATE, type LockScope, type LockState } from '@/shared/types';
|
||||
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');
|
||||
@@ -11,40 +11,16 @@ app.innerHTML = `
|
||||
<p class="subtitle">Trading companion</p>
|
||||
</header>
|
||||
|
||||
<section class="connection" id="connection"></section>
|
||||
<section class="status" id="status"></section>
|
||||
|
||||
<section class="dev-controls">
|
||||
<h2>Dev mock controls</h2>
|
||||
<p class="hint">No backend connected yet — use this to simulate a lock signal.</p>
|
||||
|
||||
<label class="field">
|
||||
<span>Locked</span>
|
||||
<input type="checkbox" id="locked-toggle" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Scope</span>
|
||||
<select id="scope-select">
|
||||
<option value="buy-only">buy-only</option>
|
||||
<option value="full-block">full-block</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Reason</span>
|
||||
<input type="text" id="reason-input" placeholder="e.g. -$62 loss crossed your $50 threshold" />
|
||||
</label>
|
||||
</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;
|
||||
const lockedToggle = document.getElementById('locked-toggle') as HTMLInputElement;
|
||||
const scopeSelect = document.getElementById('scope-select') as HTMLSelectElement;
|
||||
const reasonInput = document.getElementById('reason-input') as HTMLInputElement;
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
const div = document.createElement('div');
|
||||
@@ -52,6 +28,17 @@ function escapeHtml(value: string): string {
|
||||
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>
|
||||
@@ -60,42 +47,25 @@ function renderStatus(state: LockState): void {
|
||||
: `<div class="badge badge-unlocked">Unlocked</div>`;
|
||||
}
|
||||
|
||||
function syncFormFromState(state: LockState): void {
|
||||
lockedToggle.checked = state.locked;
|
||||
scopeSelect.value = state.scope;
|
||||
reasonInput.value = state.reason ?? '';
|
||||
}
|
||||
|
||||
async function loadState(): Promise<LockState> {
|
||||
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 pushState(): Promise<void> {
|
||||
const next: LockState = {
|
||||
locked: lockedToggle.checked,
|
||||
scope: scopeSelect.value as LockScope,
|
||||
reason: reasonInput.value.trim() || undefined,
|
||||
lockedAt: lockedToggle.checked ? Date.now() : undefined,
|
||||
};
|
||||
const message: NexaMessage = { type: 'nexa:set-lock-state', state: next };
|
||||
await browser.runtime.sendMessage(message);
|
||||
renderStatus(next);
|
||||
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;
|
||||
}
|
||||
|
||||
for (const el of [lockedToggle, scopeSelect, reasonInput]) {
|
||||
el.addEventListener('change', () => void pushState());
|
||||
}
|
||||
|
||||
loadState().then((state) => {
|
||||
syncFormFromState(state);
|
||||
renderStatus(state);
|
||||
});
|
||||
loadLockState().then(renderStatus);
|
||||
loadConnectionStatus().then(renderConnection);
|
||||
|
||||
browser.runtime.onMessage.addListener((message: NexaMessage) => {
|
||||
if (message?.type === 'nexa:lock-state-changed') {
|
||||
syncFormFromState(message.state);
|
||||
renderStatus(message.state);
|
||||
} else if (message?.type === 'nexa:connection-status-changed') {
|
||||
renderConnection(message.status);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user