Skip to content

fix: persist alerts, analyses, and chat history across page refresh#2

Merged
atomize merged 1 commit into
developfrom
fix/persist-user-state-on-refresh
Mar 29, 2026
Merged

fix: persist alerts, analyses, and chat history across page refresh#2
atomize merged 1 commit into
developfrom
fix/persist-user-state-on-refresh

Conversation

@atomize

@atomize atomize commented Mar 29, 2026

Copy link
Copy Markdown
Owner

Problem

In multi-tenant mode, refreshing the browser wiped all user-facing state:

  • Alerts & Analyses were stored in global in-memory arrays — lost on page refresh and server restart, with no per-user isolation
  • Chat history was kept in an in-memory Map — gone on refresh, and the Claude API lost all conversation context
  • Watchlists, schedule config, budget status, and reports were persisted in SQLite but only sent to the dashboard when the user manually navigated to each tab (via useEffect in each component) — so on refresh the dashboard appeared empty until you clicked around

Changes

3 new SQLite tables (db.ts)

Table Purpose
chat_history Per-user chat messages (role, content, timestamp)
alerts_log Per-user alert payloads (JSON, indexed by user + time)
analyses_log Per-user analysis payloads (JSON, indexed by user + time)

All three auto-migrate via the existing migrateV2Tables() path.

Eager state push on auth (broadcaster.ts)

New sendPerUserState() runs immediately after successful authentication and pushes:

  • Alert history (from alerts_log)
  • Analysis history (from analyses_log)
  • Chat history (from chat_history)
  • Agent config, watchlist data, schedule config, budget status, and reports

This means the dashboard is fully populated the moment the JWT is verified — no more empty panels.

Write-through persistence (broadcaster.ts)

Every alert and analysis is now written to the DB (for all users) at the point it's broadcast, covering:

  • Streamer-triggered alerts
  • Injected test alerts
  • Orchestrator analyses
  • Direct WS-submitted analyses

Chat persistence (chatHandler.ts)

  • User and assistant messages written to chat_history on each exchange
  • On first message after reconnect, the in-memory conversation cache is seeded from DB so Claude retains full context
  • clearChat also truncates the DB table

Test plan

  • Login in multi-tenant mode → verify all panels (watchlist, alerts, reports, chat, settings) are populated immediately
  • Send a chat message, refresh the page → verify chat history persists
  • Trigger a test alert, refresh → verify alert appears in the feed
  • Run a manual analysis, refresh → verify analysis appears in AI Analysis tab
  • Clear chat → verify DB is also cleared (no stale messages on next refresh)
  • Single-tenant mode (no encryption key) → verify existing in-memory behavior is unchanged

Made with Cursor


Note

Medium Risk
Adds new SQLite persistence and readback paths for user-facing realtime state and eagerly rehydrates state on auth; main risk is schema migration and increased write volume (fan-out inserts to all users) affecting DB performance or storage.

Overview
Persists previously in-memory dashboard state in multi-tenant mode by adding SQLite-backed storage for chat history, alerts, and analyses, and wiring broadcaster/chat flows to write-through these logs.

On successful auth, the WS server now sends a per-user state bundle (recent alerts/analyses, chat history, plus existing agent/watchlist/schedule/budget/reports) so a refreshed dashboard is immediately populated without extra client-side requests.

Written by Cursor Bugbot for commit 6b82518. This will update automatically on new commits. Configure here.

In multi-tenant mode, refreshing the dashboard lost all user state
because alerts, analyses, and chat were stored only in memory. Added
three DB tables (chat_history, alerts_log, analyses_log) and eagerly
push per-user state (watchlists, config, budget, reports) on auth so
the dashboard is fully populated immediately after reconnect.

Made-with: Cursor
@atomize
atomize merged commit 75a5730 into develop Mar 29, 2026
2 checks passed
@atomize
atomize deleted the fix/persist-user-state-on-refresh branch March 29, 2026 18:32

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

const errorMsg = `Error: ${errMsg}`
if (isEncryptionEnabled()) {
try { appendChatMessage(userId, 'assistant', errorMsg) } catch { /* best-effort */ }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error messages persisted to DB but not in-memory

Medium Severity

When the Claude API call fails, the error message is persisted to the DB via appendChatMessage as an 'assistant' role message but is never pushed to the in-memory history array. On reconnect, getChatHistoryDb loads DB rows (including error messages) into the conversation context sent to Claude. This creates a state divergence: before restart, Claude's context has a dangling user message with no assistant response; after restart, Claude sees "Error: ..." as its own prior response. The asymmetry changes Claude's behavior depending on whether a restart occurred.

Additional Locations (1)
Fix in Cursor Fix in Web

content: r.content,
timestamp: r.created_at,
}))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed getChatHistoryForUser is exported but never called

Low Severity

getChatHistoryForUser was renamed from getChatHistory to avoid a naming collision with the db.ts export, but no module imports or calls it. The broadcaster.ts calls getChatHistory from db.js directly and formats the results inline. This function — which also pre-populates the in-memory conversations cache — is dead code.

Fix in Cursor Fix in Web

export function appendAlertLog(userId: number, alertId: string, payload: string): void {
getDb().prepare(
"INSERT INTO alerts_log (user_id, alert_id, payload) VALUES (?, ?, ?)"
).run(userId, alertId, payload)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused per-user DB functions never called anywhere

Low Severity

appendAlertLog and appendAnalysisLog are newly added exported functions that are never called anywhere in the codebase. Only their ForAllUsers counterparts are used by broadcaster.ts. These are dead code that adds maintenance burden.

Additional Locations (1)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant