package banking import ( "context" "fmt" "strings" "time" tea "github.com/charmbracelet/bubbletea" "code.t-juice.club/torjus/oubliette/internal/shell" ) type screen int const ( screenLogin screen = iota screenMenu screenAccountSummary screenAccountDetail screenTransfer screenHistory screenMessages screenChangePin screenAdmin ) type model struct { sess *shell.SessionContext bankName string terminalID string region string state *bankState screen screen quitting bool height int login loginModel menu menuModel summary accountSummaryModel detail accountDetailModel transfer transferModel history historyModel messages messagesModel admin adminModel changePin changePinModel } func newModel(sess *shell.SessionContext, bankName, terminalID, region string) *model { state := newBankState() unread := 0 for _, msg := range state.Messages { if msg.Unread { unread++ } } return &model{ sess: sess, bankName: bankName, terminalID: terminalID, region: region, state: state, screen: screenLogin, login: newLoginModel(bankName), } } func (m *model) Init() tea.Cmd { return nil } func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.quitting { return m, tea.Quit } switch msg := msg.(type) { case tea.WindowSizeMsg: m.height = msg.Height return m, nil case tea.KeyMsg: if msg.Type == tea.KeyCtrlC { m.quitting = true return m, tea.Quit } } switch m.screen { case screenLogin: return m.updateLogin(msg) case screenMenu: return m.updateMenu(msg) case screenAccountSummary: return m.updateAccountSummary(msg) case screenAccountDetail: return m.updateAccountDetail(msg) case screenTransfer: return m.updateTransfer(msg) case screenHistory: return m.updateHistory(msg) case screenMessages: return m.updateMessages(msg) case screenChangePin: return m.updateChangePin(msg) case screenAdmin: return m.updateAdmin(msg) } return m, nil } func (m *model) View() string { var content string switch m.screen { case screenLogin: content = m.login.View() case screenMenu: content = m.menu.View() case screenAccountSummary: content = m.summary.View() case screenAccountDetail: content = m.detail.View() case screenTransfer: content = m.transfer.View() case screenHistory: content = m.history.View() case screenMessages: content = m.messages.View() case screenChangePin: content = m.changePin.View() case screenAdmin: content = m.admin.View() } return screenFrame(m.bankName, m.terminalID, m.region, content, m.height) } // --- Screen update handlers --- func (m *model) updateLogin(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.login, cmd = m.login.Update(msg) if m.login.stage == 2 { // Login always succeeds — this is a honeypot. logCmd := logAction(m.sess, fmt.Sprintf("LOGIN acct=%s", m.login.accountNum), "ACCESS GRANTED") clearCmd := m.goToMenu() return m, tea.Batch(cmd, logCmd, clearCmd) } return m, cmd } func (m *model) updateMenu(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.menu, cmd = m.menu.Update(msg) if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.Type == tea.KeyEnter { choice := strings.TrimSpace(m.menu.choice) switch choice { case "1": m.screen = screenAccountSummary m.summary = newAccountSummaryModel(m.state.Accounts) return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 1", "ACCOUNT SUMMARY")) case "2": m.screen = screenAccountDetail m.detail = newAccountDetailModel(m.state.Accounts, m.state.Transactions) return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 2", "ACCOUNT DETAIL")) case "3": m.screen = screenTransfer m.transfer = newTransferModel() return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 3", "WIRE TRANSFER")) case "4": m.screen = screenHistory m.history = newHistoryModel(m.state.Accounts, m.state.Transactions) return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 4", "TRANSACTION HISTORY")) case "5": m.screen = screenMessages m.messages = newMessagesModel(m.state.Messages) return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 5", "SECURE MESSAGES")) case "6": m.screen = screenChangePin m.changePin = newChangePinModel() return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "MENU 6", "CHANGE PIN")) case "7": m.quitting = true return m, tea.Batch(logAction(m.sess, "LOGOUT", "SESSION ENDED"), tea.Quit) case "99", "admin", "ADMIN": m.screen = screenAdmin m.admin = newAdminModel() return m, tea.Batch(tea.ClearScreen, logAction(m.sess, "ADMIN ACCESS ATTEMPT", "ADMIN SCREEN SHOWN")) } // Invalid choice, reset. m.menu.choice = "" } return m, cmd } func (m *model) updateAccountSummary(msg tea.Msg) (tea.Model, tea.Cmd) { if _, ok := msg.(tea.KeyMsg); ok { return m, m.goToMenu() } return m, nil } func (m *model) updateAccountDetail(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.detail, cmd = m.detail.Update(msg) if m.detail.choice == "back" { return m, tea.Batch(cmd, m.goToMenu()) } return m, cmd } func (m *model) updateTransfer(msg tea.Msg) (tea.Model, tea.Cmd) { prevStep := m.transfer.step var cmd tea.Cmd m.transfer, cmd = m.transfer.Update(msg) // Transfer cancelled. if m.transfer.confirm == "cancelled" { clearCmd := m.goToMenu() return m, tea.Batch(clearCmd, logAction(m.sess, "WIRE TRANSFER CANCELLED", "USER CANCELLED")) } // Clear screen when transfer steps change content height significantly // (e.g. confirm→authcode, fields→confirm, authcode→complete). if m.transfer.step != prevStep { cmd = tea.Batch(cmd, tea.ClearScreen) } // Transfer completed — log it. if m.transfer.step == transferStepComplete && prevStep != transferStepComplete { t := m.transfer.transfer m.state.Transfers = append(m.state.Transfers, t) logMsg := fmt.Sprintf("WIRE TRANSFER: routing=%s dest=%s beneficiary=%s bank=%s amount=%s memo=%s auth=%s", t.RoutingNumber, t.DestAccount, t.Beneficiary, t.BankName, t.Amount, t.Memo, t.AuthCode) return m, tea.Batch(cmd, logAction(m.sess, logMsg, "TRANSFER QUEUED")) } // Completed screen → any key goes back. if m.transfer.step == transferStepComplete { if _, ok := msg.(tea.KeyMsg); ok && prevStep == transferStepComplete { return m, tea.Batch(cmd, m.goToMenu()) } } return m, cmd } func (m *model) updateHistory(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.history, cmd = m.history.Update(msg) if m.history.choice == "back" { return m, tea.Batch(cmd, m.goToMenu()) } return m, cmd } func (m *model) updateMessages(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.messages, cmd = m.messages.Update(msg) if m.messages.choice == "back" { return m, tea.Batch(cmd, m.goToMenu()) } // Log when viewing a message. if m.messages.viewing >= 0 { idx := m.messages.viewing return m, tea.Batch(cmd, logAction(m.sess, fmt.Sprintf("VIEW MESSAGE #%d", idx+1), m.state.Messages[idx].Subj)) } return m, cmd } func (m *model) updateChangePin(msg tea.Msg) (tea.Model, tea.Cmd) { prevStage := m.changePin.stage var cmd tea.Cmd m.changePin, cmd = m.changePin.Update(msg) // Log successful PIN change. if m.changePin.stage == 3 && prevStage != 3 { cmd = tea.Batch(cmd, logAction(m.sess, "CHANGE PIN", "PIN CHANGED SUCCESSFULLY")) } if m.changePin.done { return m, tea.Batch(cmd, m.goToMenu()) } return m, cmd } func (m *model) updateAdmin(msg tea.Msg) (tea.Model, tea.Cmd) { prevLocked := m.admin.locked prevAttempts := m.admin.attempts // Check for ESC before delegating. if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.Type == tea.KeyEscape && !m.admin.locked { return m, m.goToMenu() } var cmd tea.Cmd m.admin, cmd = m.admin.Update(msg) // Log failed attempts. if m.admin.attempts > prevAttempts { cmd = tea.Batch(cmd, logAction(m.sess, fmt.Sprintf("ADMIN PIN ATTEMPT #%d", m.admin.attempts), "INVALID CREDENTIALS")) } // Log lockout. if m.admin.locked && !prevLocked { cmd = tea.Batch(cmd, tea.ClearScreen, logAction(m.sess, "ADMIN LOCKOUT", "TERMINAL LOCKED - INCIDENT LOGGED")) } // If locked and any key pressed, go back. if m.admin.locked { if _, ok := msg.(tea.KeyMsg); ok && prevLocked { return m, tea.Batch(cmd, m.goToMenu()) } } return m, cmd } func (m *model) goToMenu() tea.Cmd { unread := 0 for _, msg := range m.state.Messages { if msg.Unread { unread++ } } m.screen = screenMenu m.menu = newMenuModel(m.bankName, unread) return tea.ClearScreen } // logAction returns a tea.Cmd that logs an action to the session store. func logAction(sess *shell.SessionContext, input, output string) tea.Cmd { return func() tea.Msg { if sess.Store != nil { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() _ = sess.Store.AppendSessionLog(ctx, sess.SessionID, input, output) } if sess.OnCommand != nil { sess.OnCommand("banking") } return nil } }