Store the person uuid the master issues, so renames keep our identity

The welcome now carries a personId; it lands in the instance_id setting
and rides on every hello and HTTP report from then on. The master keys
the person by that uuid, so changing instance_name moves our history to
the new name instead of starting a stranger. Read-only in Settings on
purpose — editing it by hand would detach this machine from its person.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-09-02 12:07:25 -05:00
co-authored by Claude Fable 5
parent de9cbffd83
commit a088381749
6 changed files with 39 additions and 5 deletions
+3
View File
@@ -9,6 +9,9 @@ export async function GET() {
for (const key of VALID_KEYS) { for (const key of VALID_KEYS) {
result[key] = getSetting(key); result[key] = getSetting(key);
} }
// Read-only on purpose (not in VALID_KEYS): the master issues this uuid,
// and editing it by hand would detach the instance from its person.
result.instance_id = getSetting('instance_id');
return NextResponse.json(result); return NextResponse.json(result);
} }
+10 -1
View File
@@ -17,6 +17,7 @@ interface AppSettings {
max_concurrent_accounts: string | null; max_concurrent_accounts: string | null;
master_dashboard_url: string | null; master_dashboard_url: string | null;
instance_name: string | null; instance_name: string | null;
instance_id: string | null;
trading_hours: string | null; trading_hours: string | null;
} }
@@ -34,6 +35,7 @@ export default function SettingsPage() {
} | null>(null); } | null>(null);
const [syncing, setSyncing] = useState(false); const [syncing, setSyncing] = useState(false);
const [instanceName, setInstanceName] = useState<string>(''); const [instanceName, setInstanceName] = useState<string>('');
const [instanceId, setInstanceId] = useState<string>('');
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
@@ -75,6 +77,9 @@ export default function SettingsPage() {
if (s.instance_name != null) { if (s.instance_name != null) {
setInstanceName(s.instance_name); setInstanceName(s.instance_name);
} }
if (s.instance_id != null) {
setInstanceId(s.instance_id);
}
}); });
// Load cached contracts; if cache is empty, auto-resolve // Load cached contracts; if cache is empty, auto-resolve
@@ -211,7 +216,11 @@ export default function SettingsPage() {
<div> <div>
<p className="text-sm font-medium text-slate-800">Instance Name</p> <p className="text-sm font-medium text-slate-800">Instance Name</p>
<p className="text-xs text-slate-400 mt-0.5"> <p className="text-xs text-slate-400 mt-0.5">
Identifier for this autotrader on the master dashboard Identifier for this autotrader on the master dashboard.
Renaming is safe — the master tracks this machine by its
person ID{instanceId ? (
<> (<span className="font-mono">{instanceId.slice(0, 8)}</span>)</>
) : ' once it has connected'}, so history follows the new name.
</p> </p>
</div> </div>
<input <input
+4
View File
@@ -155,6 +155,10 @@ seedSetting.run('max_concurrent_accounts', '5');
seedSetting.run('tick_interval_seconds', '60'); seedSetting.run('tick_interval_seconds', '60');
seedSetting.run('master_dashboard_url', 'https://master.juicerroom.com'); seedSetting.run('master_dashboard_url', 'https://master.juicerroom.com');
seedSetting.run('instance_name', ''); seedSetting.run('instance_name', '');
// The person uuid the master issues in its welcome. Stored, then presented on
// every hello/report — it is what lets a renamed instance_name stay the same
// person on the master. Never edited by hand.
seedSetting.run('instance_id', '');
seedSetting.run('trading_hours', 'full_cme'); seedSetting.run('trading_hours', 'full_cme');
// Solves the captchas the AutoBuyer runs into. Blank uses the key hardcoded in // Solves the captchas the AutoBuyer runs into. Blank uses the key hardcoded in
// app/api/autobuyer/captcha; set this (or TWOCAPTCHA_API_KEY) to bill another // app/api/autobuyer/captcha; set this (or TWOCAPTCHA_API_KEY) to bill another
+9 -1
View File
@@ -15,7 +15,7 @@
* master you control, and prefer https so it cannot be redirected in transit. * master you control, and prefer https so it cannot be redirected in transit.
*/ */
import { getSetting, getInstruments, getRunnerHeartbeat, RUNNER_TIMEOUT_MS } from './db'; import { getSetting, setSetting, getInstruments, getRunnerHeartbeat, RUNNER_TIMEOUT_MS } from './db';
import { runTrade, startScheduler, stopScheduler, getSchedulerStatus } from './auto-trade'; import { runTrade, startScheduler, stopScheduler, getSchedulerStatus } from './auto-trade';
import { FIRMS, automationKey } from './automations'; import { FIRMS, automationKey } from './automations';
import { POINT_VALUES } from './trading-logic'; import { POINT_VALUES } from './trading-logic';
@@ -217,9 +217,14 @@ function connect() {
s.ready = false; s.ready = false;
socket.addEventListener('open', () => { socket.addEventListener('open', () => {
// The stored uuid, once the master has issued one — it, not the name,
// is who this instance is, so renaming instance_name keeps the same
// person (and their accounting history) on the master.
const instanceId = getSetting('instance_id');
socket.send(JSON.stringify({ socket.send(JSON.stringify({
type: 'hello', type: 'hello',
instance, instance,
id: instanceId || undefined,
capabilities: collectCapabilities(), capabilities: collectCapabilities(),
} satisfies InstanceMessage)); } satisfies InstanceMessage));
}); });
@@ -235,6 +240,9 @@ function connect() {
if (msg.type === 'welcome') { if (msg.type === 'welcome') {
s.ready = true; s.ready = true;
s.attempt = 0; s.attempt = 0;
if (msg.personId && msg.personId !== getSetting('instance_id')) {
setSetting('instance_id', msg.personId);
}
console.log(`[master-link] connected to ${url} as "${instance}"`); console.log(`[master-link] connected to ${url} as "${instance}"`);
return; return;
} }
+5 -2
View File
@@ -106,10 +106,13 @@ export type Command =
| { kind: 'report-now' }; | { kind: 'report-now' };
export type MasterMessage = export type MasterMessage =
| { type: 'welcome' } /** personId is the person's stable uuid; the instance stores it and
* presents it on later hellos, which is what makes a rename follow the
* person. */
| { type: 'welcome'; personId?: string }
| { type: 'command'; id: string; command: Command }; | { type: 'command'; id: string; command: Command };
export type InstanceMessage = export type InstanceMessage =
| { type: 'hello'; instance: string; capabilities: InstanceCapabilities } | { type: 'hello'; instance: string; id?: string; capabilities: InstanceCapabilities }
| { type: 'report'; firms: FirmStats[]; scheduler: SchedulerState; capabilities?: InstanceCapabilities } | { type: 'report'; firms: FirmStats[]; scheduler: SchedulerState; capabilities?: InstanceCapabilities }
| { type: 'ack'; id: string; ok: boolean; result?: unknown; error?: string }; | { type: 'ack'; id: string; ok: boolean; result?: unknown; error?: string };
+8 -1
View File
@@ -132,7 +132,14 @@ export async function sendReport(): Promise<void> {
await fetch(new URL('/api/report', url), { await fetch(new URL('/api/report', url), {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ instance: instanceName, firms, scheduler }), body: JSON.stringify({
instance: instanceName,
// Same identity the socket hello carries, so a rename resolves
// to the same person whichever transport the report takes.
id: getSetting('instance_id') || undefined,
firms,
scheduler,
}),
}); });
} catch { } catch {
// Master might be down — silently continue // Master might be down — silently continue