feat(simple): add long-term hold mode

This commit is contained in:
root 2026-05-06 17:37:04 +00:00
parent 0b526f3499
commit a1a63cc945
8 changed files with 319 additions and 26 deletions

View File

@ -65,6 +65,10 @@ const isSimpleSellEntry = (entry: ManualEntryRecord): boolean => {
return String(entry.simple_side || '').trim().toLowerCase() === 'sell';
};
const isSimpleLongTermHold = (entry: ManualEntryRecord): boolean => {
return String(entry.holding_mode || '').trim().toLowerCase() === 'long_term';
};
const isSimpleSubmittedStatus = (status?: string | null): boolean => {
return String(status || '').trim().toLowerCase() === 'simple_entry_submitted';
};
@ -291,6 +295,8 @@ async function main() {
filled_quantity: activePosition.size,
buy_time: entry.buy_time || new Date().toISOString(),
status: 'simple_bought',
holding_mode: entry.holding_mode || 'short_term',
automation_state: isSimpleLongTermHold(entry) ? 'paused_long_term' : 'holding_managed',
active: true,
});
emitSimpleSetupEvent(
@ -363,6 +369,8 @@ async function main() {
filled_quantity: result.adjustedQty ?? entry.filled_quantity,
buy_time: new Date().toISOString(),
status: 'simple_entry_submitted',
holding_mode: entry.holding_mode || 'short_term',
automation_state: 'entry_submitted',
active: true,
});
emitSimpleSetupEvent(
@ -390,6 +398,10 @@ async function main() {
continue;
}
if (isSimpleLongTermHold(entry)) {
continue;
}
const linkedTradeId = String(entry.linked_trade_id || '').trim();
const exitDedupKey = linkedTradeId ? `${symbol}::${linkedTradeId}` : symbol;
if (processedSimpleExitKeys.has(exitDedupKey)) {
@ -417,6 +429,7 @@ async function main() {
...entry,
active: false,
status: 'sellCompleted',
automation_state: 'closed',
sell_time: entry.sell_time || new Date().toISOString(),
sell_price: currentPrice,
});
@ -455,6 +468,8 @@ async function main() {
entry_price: activeEntryPrice,
filled_quantity: entry.filled_quantity ?? activePosition.size,
status: 'simple_exit_submitted',
holding_mode: entry.holding_mode || 'short_term',
automation_state: 'exit_submitted',
active: true,
});
emitSimpleSetupEvent(
@ -514,6 +529,8 @@ async function main() {
filled_quantity: event.fillQty || simpleEntry.filled_quantity || simpleEntry.quantity,
buy_time: simpleEntry.buy_time || new Date().toISOString(),
status: 'simple_bought',
holding_mode: simpleEntry.holding_mode || 'short_term',
automation_state: isSimpleLongTermHold(simpleEntry) ? 'paused_long_term' : 'holding_managed',
active: true,
});
emitSimpleSetupEvent(
@ -534,6 +551,7 @@ async function main() {
await saveManualEntryForUser(simpleEntry.user_id, {
...simpleEntry,
status: 'simple_armed_buy',
automation_state: 'armed',
active: true,
buy_time: null,
entry_price: null,
@ -557,6 +575,7 @@ async function main() {
await saveManualEntryForUser(simpleExitEntry.user_id, {
...simpleExitEntry,
status: 'simple_bought',
automation_state: isSimpleLongTermHold(simpleExitEntry) ? 'paused_long_term' : 'holding_managed',
active: true,
});
emitSimpleSetupEvent(
@ -603,6 +622,7 @@ async function main() {
await saveManualEntryForUser(simpleExitEntry.user_id, {
...simpleExitEntry,
status: 'simple_bought',
automation_state: isSimpleLongTermHold(simpleExitEntry) ? 'paused_long_term' : 'holding_managed',
active: true,
filled_quantity: applied.remainingSize,
});
@ -621,6 +641,7 @@ async function main() {
...simpleExitEntry,
active: false,
status: 'sellCompleted',
automation_state: 'closed',
sell_time: simpleExitEntry.sell_time || new Date().toISOString(),
sell_price: event.fillPrice || active.entryPrice,
});

View File

@ -40,6 +40,8 @@ export interface ManualEntryRecord {
drop_trigger_mode?: string | null;
profit_target_mode?: string | null;
linked_trade_id?: string | null;
holding_mode?: string | null;
automation_state?: string | null;
}
type ManualEntryDocument = ManualEntryRecord & {
@ -64,7 +66,42 @@ function normalizeNullableString(value: unknown): string | null | undefined {
return text ? text : null;
}
function deriveSimpleAutomationState(status: string, holdingMode: string | null | undefined): string | null {
const normalizedStatus = String(status || '').trim().toLowerCase();
const normalizedMode = String(holdingMode || '').trim().toLowerCase();
if (normalizedMode === 'long_term') {
return normalizedStatus === 'sellcompleted' ? 'closed' : 'paused_long_term';
}
switch (normalizedStatus) {
case 'simple_armed_buy':
case 'simple_armed_sell':
return 'armed';
case 'simple_entry_submitted':
return 'entry_submitted';
case 'simple_bought':
return 'holding_managed';
case 'simple_exit_submitted':
return 'exit_submitted';
case 'sellcompleted':
return 'closed';
default:
return null;
}
}
function normalizeEntry(userId: string, input: Partial<ManualEntryRecord>, existing?: ManualEntryRecord | null): ManualEntryRecord {
const workflowType = normalizeNullableString(input.workflow_type ?? existing?.workflow_type);
const holdingMode = normalizeNullableString(
input.holding_mode
?? existing?.holding_mode
?? (String(workflowType || '').trim().toLowerCase() === 'simple' ? 'short_term' : null)
);
const status = String(input.status || existing?.status || 'active');
const automationState = normalizeNullableString(
input.automation_state
?? existing?.automation_state
?? deriveSimpleAutomationState(status, holdingMode)
);
return {
stock_instance_id: String(input.stock_instance_id || existing?.stock_instance_id || randomUUID()),
symbol: String(input.symbol || existing?.symbol || '').trim(),
@ -80,7 +117,7 @@ function normalizeEntry(userId: string, input: Partial<ManualEntryRecord>, exist
sizing_mode: normalizeNullableString(input.sizing_mode ?? existing?.sizing_mode),
filled_quantity: normalizeNullableNumber(input.filled_quantity ?? existing?.filled_quantity),
notes: normalizeNullableString(input.notes ?? existing?.notes),
status: String(input.status || existing?.status || 'active'),
status,
is_crypto: Boolean(input.is_crypto ?? existing?.is_crypto ?? false),
is_real_trade: Boolean(input.is_real_trade ?? existing?.is_real_trade ?? false),
label: normalizeNullableString(input.label ?? existing?.label),
@ -88,11 +125,13 @@ function normalizeEntry(userId: string, input: Partial<ManualEntryRecord>, exist
reference_price: normalizeNullableNumber(input.reference_price ?? existing?.reference_price),
gain_threshold_for_sell: normalizeNullableNumber(input.gain_threshold_for_sell ?? existing?.gain_threshold_for_sell),
drop_threshold_for_buy: normalizeNullableNumber(input.drop_threshold_for_buy ?? existing?.drop_threshold_for_buy),
workflow_type: normalizeNullableString(input.workflow_type ?? existing?.workflow_type),
workflow_type: workflowType,
simple_side: normalizeNullableString(input.simple_side ?? existing?.simple_side),
drop_trigger_mode: normalizeNullableString(input.drop_trigger_mode ?? existing?.drop_trigger_mode),
profit_target_mode: normalizeNullableString(input.profit_target_mode ?? existing?.profit_target_mode),
linked_trade_id: normalizeNullableString(input.linked_trade_id ?? existing?.linked_trade_id),
holding_mode: holdingMode,
automation_state: automationState,
};
}

View File

@ -30,6 +30,8 @@ export interface ManualEntryPayload {
drop_trigger_mode?: string | null;
profit_target_mode?: string | null;
linked_trade_id?: string | null;
holding_mode?: string | null;
automation_state?: string | null;
}
async function getAccessToken(): Promise<string> {

View File

@ -6,9 +6,9 @@ import type { BotState } from '../hooks/useWebSocket';
import { DEFAULT_BOT_STATE } from '../hooks/useWebSocket';
import { createManualEntry, deleteManualEntry, fetchManualEntries, type ManualEntryPayload } from '../lib/manualEntriesApi';
interface Entry {
stock_instance_id: string;
symbol: string;
interface Entry {
stock_instance_id: string;
symbol: string;
active: boolean;
user_id: string;
buy_price?: string;
@ -23,13 +23,19 @@ interface Entry {
is_real_trade: boolean;
label?: string;
entry_price?: string;
gain_threshold_for_sell?: string;
drop_threshold_for_buy?: string;
}
export const filterEntriesByTab = (entries: Entry[], activeTab: string) =>
entries.filter((entry) => {
switch (activeTab) {
gain_threshold_for_sell?: string;
drop_threshold_for_buy?: string;
workflow_type?: string;
holding_mode?: string;
automation_state?: string;
}
export const filterEntriesByTab = (entries: Entry[], activeTab: string) =>
entries.filter((entry) => {
if (String(entry.workflow_type || '').trim().toLowerCase() === 'simple') {
return false;
}
switch (activeTab) {
case 'paperActive':
return !entry.is_real_trade && entry.active && entry.status !== 'sellCompleted';
case 'paperCompleted':

View File

@ -23,11 +23,13 @@ interface HybridPosition {
pnl?: number | null;
pnlPercent?: number | null;
stopLoss?: number;
takeProfit?: number;
profileId?: string;
profileName?: string;
tradeId?: string;
}
takeProfit?: number;
profileId?: string;
profileName?: string;
tradeId?: string;
planMode?: 'short_term' | 'long_term';
planState?: string | null;
}
interface Profile {
id: string;
@ -414,7 +416,8 @@ export const assignLifecycleTradeIds = (
export const PositionsTab = ({ botState }: PositionsTabProps) => {
const { user, profile } = useAuth();
const [manualPositions, setManualPositions] = useState<HybridPosition[]>([]);
const [manualPositions, setManualPositions] = useState<HybridPosition[]>([]);
const [simplePlanMetaByTradeId, setSimplePlanMetaByTradeId] = useState<Record<string, { holdingMode: 'short_term' | 'long_term'; automationState?: string | null }>>({});
const [dbOrders, setDbOrders] = useState<RawOrderRecord[]>([]);
const [historyTradeKeys, setHistoryTradeKeys] = useState<string[]>([]);
const [profiles, setProfiles] = useState<Profile[]>([]);
@ -498,10 +501,26 @@ export const PositionsTab = ({ botState }: PositionsTabProps) => {
pnl: null,
pnlPercent: null,
stopLoss: entry.drop_threshold_for_buy,
takeProfit: entry.gain_threshold_for_sell
takeProfit: entry.gain_threshold_for_sell
})).filter((p: { size: number; entryPrice: number; }) => p.size > 0 && p.entryPrice > 0);
setManualPositions(positions);
}
setManualPositions(positions);
const nextSimplePlanMetaByTradeId = Object.fromEntries(
posData
.filter((entry: any) => String(entry.workflow_type || '').trim().toLowerCase() === 'simple')
.map((entry: any) => {
const tradeId = String(entry.linked_trade_id || '').trim();
if (!tradeId) return null;
const holdingMode = String(entry.holding_mode || '').trim().toLowerCase() === 'long_term' ? 'long_term' : 'short_term';
return [tradeId, {
holdingMode,
automationState: String(entry.automation_state || '').trim() || null,
}] as const;
})
.filter(Boolean) as Array<readonly [string, { holdingMode: 'short_term' | 'long_term'; automationState?: string | null }]>
);
setSimplePlanMetaByTradeId(nextSimplePlanMetaByTradeId);
}
setDbOrders(ordData || []);
setHistoryTradeKeys(tradeKeys);
@ -543,8 +562,14 @@ export const PositionsTab = ({ botState }: PositionsTabProps) => {
takeProfit: p.takeProfit,
profileId: p.profileId,
profileName: p.profileName,
tradeId: p.tradeId,
};
tradeId: p.tradeId,
planMode: p.tradeId && simplePlanMetaByTradeId[p.tradeId]
? simplePlanMetaByTradeId[p.tradeId].holdingMode
: undefined,
planState: p.tradeId && simplePlanMetaByTradeId[p.tradeId]
? simplePlanMetaByTradeId[p.tradeId].automationState || null
: null,
};
const tradeId = String(normalized.tradeId || '').trim();
const dedupeKey = tradeId
@ -567,7 +592,7 @@ export const PositionsTab = ({ botState }: PositionsTabProps) => {
});
}
return Array.from(deduped.values());
}, [botState.positions]);
}, [botState.positions, simplePlanMetaByTradeId]);
const managedSymbols = useMemo(() => {
return new Set(Object.keys(botState.symbols || {}).map((symbol) => String(symbol).toUpperCase()));
@ -1358,9 +1383,20 @@ export const PositionsTab = ({ botState }: PositionsTabProps) => {
<span className="text-[10px] text-zinc-700"></span>
)}
</td>
<td className="px-6 py-4 font-mono font-bold text-white">{pos.symbol}</td>
<td className="px-6 py-4 font-mono font-bold text-white">{pos.symbol}</td>
<td className="px-6 py-4">
<span className={`text-[10px] font-black ${pos.side === 'BUY' ? 'text-green-400' : 'text-red-400'}`}>{pos.side}</span>
{pos.planMode ? (
<div className="mt-1 flex flex-wrap gap-1">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-black uppercase tracking-wider border ${
pos.planMode === 'long_term'
? 'bg-amber-500/10 text-amber-300 border-amber-500/20'
: 'bg-sky-500/10 text-sky-300 border-sky-500/20'
}`}>
{pos.planMode === 'long_term' ? 'Long-term hold' : 'Short-term managed'}
</span>
</div>
) : null}
</td>
<td className="px-6 py-4 text-xs font-mono text-gray-300">
{formatDisplayQty(pos.size)}

View File

@ -70,7 +70,8 @@ describe('tab helper coverage', () => {
const entries = [
{ stock_instance_id: '1', is_real_trade: false, active: true, status: 'active' },
{ stock_instance_id: '2', is_real_trade: false, active: false, status: 'sellCompleted' },
{ stock_instance_id: '3', is_real_trade: true, active: true, status: 'active' }
{ stock_instance_id: '3', is_real_trade: true, active: true, status: 'active' },
{ stock_instance_id: '4', is_real_trade: false, active: true, status: 'simple_bought', workflow_type: 'simple' }
] as any;
expect(filterEntriesByTab(entries, 'paperActive')).toHaveLength(1);

View File

@ -42,6 +42,8 @@ describe('SimpleView helpers', () => {
profit_target_mode: 'percent',
linked_trade_id: null,
profile_id: null,
holding_mode: 'short_term',
automation_state: 'armed',
buy_price: null,
sell_price: null,
buy_time: null,
@ -96,6 +98,8 @@ describe('SimpleView helpers', () => {
profit_target_mode: 'dollar',
linked_trade_id: 'TRD-123',
profile_id: 'simple-profile',
holding_mode: 'short_term',
automation_state: 'armed',
buy_price: null,
sell_price: null,
buy_time: null,
@ -143,6 +147,8 @@ describe('SimpleView helpers', () => {
profit_target_mode: 'dollar',
linked_trade_id: null,
profile_id: null,
holding_mode: 'short_term',
automation_state: 'armed',
buy_price: null,
sell_price: null,
buy_time: null,
@ -150,6 +156,40 @@ describe('SimpleView helpers', () => {
});
});
it('preserves long-term mode when editing an existing setup', () => {
const payload = buildSimpleSetupPayload({
draft: {
symbol: 'aapl',
side: 'buy',
sizingMode: 'quantity',
quantity: '5',
amountUsd: '',
currentMarketPrice: '210.25',
dropMode: 'dollar',
dropValue: '12',
profitMode: 'percent',
profitValue: '8',
notes: 'Long-term compounder',
},
existingId: 'simple-1',
existingEntry: {
stock_instance_id: 'simple-1',
symbol: 'AAPL',
active: true,
status: 'simple_bought',
is_crypto: false,
is_real_trade: false,
workflow_type: 'simple',
simple_side: 'buy',
holding_mode: 'long_term',
automation_state: 'paused_long_term',
} as any,
});
expect(payload.holding_mode).toBe('long_term');
expect(payload.automation_state).toBe('paused_long_term');
});
it('rejects sell setups without an existing holding', () => {
expect(() => buildSimpleSetupPayload({
draft: {

View File

@ -115,6 +115,35 @@ function normalizeMode(value: unknown, fallback: TriggerMode = 'percent'): Trigg
return String(value || '').trim().toLowerCase() === 'dollar' ? 'dollar' : fallback;
}
function normalizeHoldingMode(value: unknown): 'short_term' | 'long_term' {
return String(value || '').trim().toLowerCase() === 'long_term' ? 'long_term' : 'short_term';
}
function normalizeAutomationState(value: unknown, entry: ManualEntryPayload): string {
const explicit = String(value || '').trim().toLowerCase();
if (explicit) return explicit;
const status = String(entry.status || '').trim().toLowerCase();
const holdingMode = normalizeHoldingMode(entry.holding_mode);
if (holdingMode === 'long_term') {
return status === 'sellcompleted' ? 'closed' : 'paused_long_term';
}
switch (status) {
case 'simple_armed_buy':
case 'simple_armed_sell':
return 'armed';
case 'simple_entry_submitted':
return 'entry_submitted';
case 'simple_bought':
return 'holding_managed';
case 'simple_exit_submitted':
return 'exit_submitted';
case 'sellcompleted':
return 'closed';
default:
return 'unknown';
}
}
function computeBuyTriggerPrice(draft: SimpleSetupDraft): number | null {
const currentMarketPrice = parsePositiveNumber(draft.currentMarketPrice);
const dropValue = parseNonNegativeNumber(draft.dropValue);
@ -145,6 +174,7 @@ export function buildSimpleSetupPayload(input: {
draft: SimpleSetupDraft;
existingId?: string;
holding?: SimpleHolding | null;
existingEntry?: ManualEntryPayload | null;
}): ManualEntryPayload {
const symbol = input.draft.symbol.trim().toUpperCase();
if (!symbol) {
@ -166,6 +196,9 @@ export function buildSimpleSetupPayload(input: {
const side = input.draft.side;
const holding = input.holding || null;
const existingEntry = input.existingEntry || null;
const holdingMode = normalizeHoldingMode(existingEntry?.holding_mode);
const automationState = normalizeAutomationState(existingEntry?.automation_state, existingEntry || { status: side === 'buy' ? 'simple_armed_buy' : 'simple_armed_sell' } as ManualEntryPayload);
if (side === 'sell' && !holding) {
throw new Error('Sell setups require an existing Simple holding for this symbol.');
@ -208,6 +241,8 @@ export function buildSimpleSetupPayload(input: {
profit_target_mode: input.draft.profitMode,
linked_trade_id: side === 'sell' ? holding!.tradeId || null : null,
profile_id: side === 'sell' ? holding!.profileId || null : null,
holding_mode: existingEntry ? holdingMode : 'short_term',
automation_state: existingEntry ? automationState : 'armed',
buy_price: null,
sell_price: null,
buy_time: null,
@ -341,6 +376,30 @@ function formatSetupStatus(status?: string | null): string {
}
}
function formatHoldingMode(mode?: string | null): string {
return normalizeHoldingMode(mode) === 'long_term' ? 'Long-term' : 'Short-term';
}
function formatAutomationState(entry: ManualEntryPayload): string {
const state = normalizeAutomationState(entry.automation_state, entry);
switch (state) {
case 'armed':
return 'Automation armed';
case 'entry_submitted':
return 'Entry syncing';
case 'holding_managed':
return 'Exit managed';
case 'paused_long_term':
return 'Automation paused';
case 'exit_submitted':
return 'Exit syncing';
case 'closed':
return 'Closed';
default:
return 'State syncing';
}
}
function statusToneClasses(tone: SimpleRuntimeSnapshot['tone']): string {
switch (tone) {
case 'success':
@ -395,11 +454,18 @@ function describeNextAction(
const symbol = String(entry.symbol || '').trim().toUpperCase() || 'This symbol';
if (!runtimeSnapshot) {
if (normalizeHoldingMode(entry.holding_mode) === 'long_term') {
return `${symbol} is being kept as a long-term hold. Automated exit monitoring is paused until you resume it.`;
}
return side === 'buy'
? `${symbol} is saved and waiting for the configured buy trigger.`
: `${symbol} is saved and waiting for an eligible holding to manage.`;
}
if (normalizeHoldingMode(entry.holding_mode) === 'long_term' && runtimeSnapshot.stage === 'filled') {
return `${symbol} is held as a long-term position. No automated profit exit is currently armed.`;
}
switch (runtimeSnapshot.stage) {
case 'armed':
return side === 'buy'
@ -708,6 +774,15 @@ export function SimpleView() {
setSavedSetups(normalizeSimpleEntries(entryRows));
}
async function updateSavedSetup(entryId: string, updater: (entry: ManualEntryPayload) => ManualEntryPayload) {
const existing = savedSetups.find((entry) => String(entry.stock_instance_id || '') === entryId);
if (!existing) return;
const updated = await updateManualEntry(entryId, updater(existing));
setSavedSetups((prev) => normalizeSimpleEntries(prev.map((entry) => (
String(entry.stock_instance_id || '') === entryId ? updated : entry
))));
}
function setMarketPriceValue(value: string, source: MarketPriceSource) {
setMarketPriceSource(source);
updateDraft('currentMarketPrice', value);
@ -764,10 +839,14 @@ export function SimpleView() {
setMessage(null);
try {
const existingEntry = editingSetupId
? (savedSetups.find((entry) => String(entry.stock_instance_id || '') === editingSetupId) || null)
: null;
const payload = buildSimpleSetupPayload({
draft,
existingId: editingSetupId || undefined,
holding: draft.side === 'sell' ? matchingHolding : null,
existingEntry,
});
if (editingSetupId) {
@ -818,6 +897,44 @@ export function SimpleView() {
}
}
async function handleConvertToLongTerm(entry: ManualEntryPayload) {
const entryId = String(entry.stock_instance_id || '');
if (!entryId) return;
setError(null);
setMessage(null);
try {
await updateSavedSetup(entryId, (current) => ({
...current,
holding_mode: 'long_term',
automation_state: 'paused_long_term',
status: 'simple_bought',
active: true,
}));
setMessage(`${String(entry.symbol || '').trim().toUpperCase()} is now treated as a long-term hold. Automated exit monitoring is paused.`);
} catch (err: any) {
setError(err?.message ?? 'Failed to convert setup to long-term mode');
}
}
async function handleResumeExitManagement(entry: ManualEntryPayload) {
const entryId = String(entry.stock_instance_id || '');
if (!entryId) return;
setError(null);
setMessage(null);
try {
await updateSavedSetup(entryId, (current) => ({
...current,
holding_mode: 'short_term',
automation_state: 'holding_managed',
status: 'simple_bought',
active: true,
}));
setMessage(`${String(entry.symbol || '').trim().toUpperCase()} is back under short-term exit management.`);
} catch (err: any) {
setError(err?.message ?? 'Failed to resume short-term exit management');
}
}
const saveButtonLabel = editingSetupId ? 'Update setup' : 'Save setup';
const saveButtonDisabled = submitting || loadingPrice || (draft.side === 'sell' && !matchingHolding);
@ -1139,6 +1256,9 @@ export function SimpleView() {
const nextActionText = describeNextAction(entry, runtimeSnapshot);
const updatedAt = formatSetupUpdatedAt(entry);
const eventHistory = deriveSimpleEventHistory(entry, runtimeEvents);
const holdingMode = normalizeHoldingMode(entry.holding_mode);
const canConvertToLongTerm = side === 'buy' && holdingMode === 'short_term' && runtimeSnapshot?.stage === 'filled';
const canResumeExitManagement = side === 'buy' && holdingMode === 'long_term' && runtimeSnapshot?.stage === 'filled';
return (
<div key={entryId} className="rounded-[1.5rem] border border-[var(--border)] bg-[var(--card-elevated)] p-5">
<div className="mb-3 flex items-start justify-between gap-4">
@ -1154,6 +1274,28 @@ export function SimpleView() {
<p className="mt-2 text-sm text-[var(--muted-foreground)]">{describeSavedSetup(entry)}</p>
</div>
<div className="flex items-center gap-2">
{canConvertToLongTerm ? (
<Button
type="button"
onClick={() => void handleConvertToLongTerm(entry)}
variant="outline"
size="sm"
className="uppercase tracking-[0.18em]"
>
Convert to long-term
</Button>
) : null}
{canResumeExitManagement ? (
<Button
type="button"
onClick={() => void handleResumeExitManagement(entry)}
variant="outline"
size="sm"
className="uppercase tracking-[0.18em]"
>
Resume exit management
</Button>
) : null}
<Button
type="button"
onClick={() => handleEdit(entry)}
@ -1185,6 +1327,12 @@ export function SimpleView() {
<span className="rounded-full border border-[var(--border)] px-3 py-1">
{formatSetupStatus(entry.status)}
</span>
<span className="rounded-full border border-[var(--border)] px-3 py-1">
{formatHoldingMode(entry.holding_mode)}
</span>
<span className="rounded-full border border-[var(--border)] px-3 py-1">
{formatAutomationState(entry)}
</span>
{runtimeSnapshot ? (
<span className={`rounded-full border px-3 py-1 ${statusToneClasses(runtimeSnapshot.tone)}`}>
{runtimeSnapshot.label}