# ==============================================================================
# Shiny Hit-Monitor & Dual-Panel Exposure Dashboard  (v7.1, Aug 19 2026)
# v7.1: zero new dependencies (digest removed; hashing via openssl, already a
#       dependency) and namespace-robust access to cgg_boundary_series() so the
#       app degrades gracefully instead of dying if the revr reinstall lags.
# Left Panel:  Signal log & Completed trades (unchanged)
# Right Panel: Two collapsible stacked groups —
#              (1) Open trades telemetry cards (own scroll region)
#              (2) Live Boundary Chart ("real-time slide 13"): price vs
#                  evolving Phi/Plo 51° forecast bands, per-REIT dropdown
# Anti-pulse architecture: every output is digest-gated (re-renders ONLY when
# its own payload changes) and telemetry cards are per-symbol outputs, so a new
# tick never rebuilds the panel DOM — no scroll jostle, no input resets.
# Includes Secure Cryptographic Authentication (SHA-256 Hashed)
# CBQT proprietary. August 2026.
# ==============================================================================

library(shiny)
library(tidyverse)
library(openssl)
library(revr)

# Find available price dates
available_dates <- tryCatch({
  dir("/data/local3/prices/") %>%
    str_extract("^[0-9]{4}-[0-9]{2}-[0-9]{2}") %>%
    compact() %>%
    sort(decreasing = TRUE)
}, error = function(e) as.character(Sys.Date()))

default_date <- if (length(available_dates) > 0) available_dates[1] else as.character(Sys.Date())

# Cryptographic SHA-256 password hashes (never stored in plaintext)
AUTH_HASHES <- list(
  "joshua"  = Sys.getenv("HIT_MONITOR_HASH_JOSHUA",  "c504be8509f23133d561873124557405965ceba343ca0e9b1b7be4626216eef4"),
  "andreas" = Sys.getenv("HIT_MONITOR_HASH_ANDREAS", "c3cd3eabfb0c645385d83bfe9228f700cdf7296d14695185c673386ad46a5750")
)

ui <- fluidPage(
  tags$head(
    tags$style(HTML("
      body { background-color: #0d1117; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 10px; overflow: hidden; }
      .header-card { background-color: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 8px 15px; margin-bottom: 10px; }
      .metric-box { background-color: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 6px; text-align: center; }
      .metric-val { font-size: 20px; font-weight: bold; }
      .metric-lbl { font-size: 11px; text-transform: uppercase; color: #8b949e; letter-spacing: 0.5px; }
      .panel-card { background-color: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 10px 15px; height: calc(100vh - 210px); }
      .panel-card-right { background-color: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 10px 15px; height: calc(100vh - 210px); overflow-y: auto; }
      .terminal-log { background-color: #030712; color: #38ef7d; font-family: 'JetBrains Mono', 'Fira Code', Menlo, monospace; font-size: 12px; padding: 12px; border-radius: 6px; border: 1px solid #1f2937; height: calc(100vh - 265px); overflow-y: scroll; line-height: 1.5; will-change: transform; transform: translateZ(0); -webkit-overflow-scrolling: touch; }
      .scroll-container { height: calc(100vh - 265px); overflow-y: auto; will-change: transform; transform: translateZ(0); -webkit-overflow-scrolling: touch; }
      .telemetry-scroll { max-height: 46vh; overflow-y: auto; will-change: transform; transform: translateZ(0); -webkit-overflow-scrolling: touch; }
      .table { color: #c9d1d9; border-color: #30363d; background-color: #161b22; }
      .table > thead > tr > th { border-bottom: 2px solid #30363d !important; color: #58a6ff; font-weight: bold; background-color: #161b22; }
      .table > tbody > tr > td { border-top: 1px solid #30363d !important; background-color: #161b22; color: #c9d1d9; }
      .table-striped > tbody > tr:nth-of-type(odd) > td { background-color: #0d1117 !important; }
      .table-striped > tbody > tr:nth-of-type(even) > td { background-color: #161b22 !important; }
      .table-hover > tbody > tr:hover > td { background-color: #21262d !important; }
      .table-bordered { border: 1px solid #30363d !important; }
      .table-bordered > thead > tr > th, .table-bordered > tbody > tr > td { border: 1px solid #30363d !important; }
      .pos-card { background-color: #21262d; border: 1px solid #30363d; border-radius: 6px; padding: 12px; margin-bottom: 12px; }
      .pos-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #30363d; padding-bottom: 8px; margin-bottom: 8px; }
      .badge-long { background-color: #238636; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: bold; }
      .badge-short { background-color: #da3633; color: white; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: bold; }
      .pnl-pos { color: #3fb950; font-weight: bold; font-size: 16px; }
      .pnl-neg { color: #f85149; font-weight: bold; font-size: 16px; }
      .sub-metric { font-size: 11px; color: #8b949e; }
      .sub-val { font-size: 12px; color: #c9d1d9; font-weight: 600; }
      .telemetry-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; }
      .tag-spike { color: #f0883e; font-weight: bold; font-size: 11px; }
      .tag-state { color: #58a6ff; font-weight: bold; font-size: 11px; }
      .tag-cut { background-color: #8e1519; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: bold; }
      .progress { height: 8px; margin-bottom: 4px; background-color: #30363d; }
      .btn-custom { background-color: #238636; color: white; border: none; font-weight: 600; }
      .btn-custom:hover { background-color: #2ea043; color: white; }
      .btn-logout { background-color: #21262d; border: 1px solid #30363d; color: #8b949e; font-size: 11px; padding: 5px 10px; border-radius: 4px; }
      .btn-logout:hover { background-color: #30363d; color: white; }
      .user-badge { background-color: #1f242c; border: 1px solid #30363d; color: #58a6ff; font-size: 11px; padding: 5px 10px; border-radius: 4px; font-weight: 600; }
      @keyframes blinker { 50% { opacity: 0.3; } }
      .blink { animation: blinker 1.5s linear infinite; color: #38ef7d; }

      /* Collapsible stacked groups (right panel) */
      .collap-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; padding: 6px 4px; border-bottom: 1px solid #30363d; margin-bottom: 8px; }
      .collap-header:hover { background-color: #1c2128; border-radius: 4px; }
      .collap-title { margin: 0; font-weight: bold; }
      .chev { transition: transform 0.15s ease; color: #8b949e; font-size: 12px; }
      .chev.rot { transform: rotate(-90deg); }
      .collap-body { overflow: hidden; }
      .chart-frame { background-color: #ffffff; border-radius: 6px; padding: 6px; }

      /* Login Card Styling */
      .login-container { display: flex; justify-content: center; align-items: center; min-height: 80vh; }
      .login-card { background-color: #161b22; border: 1px solid #30363d; border-radius: 12px; padding: 35px 30px; width: 380px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); }
      .login-title { color: #58a6ff; font-weight: 800; font-size: 20px; text-align: center; margin-bottom: 4px; letter-spacing: 1px; }
      .login-sub { color: #8b949e; font-size: 12px; text-align: center; margin-bottom: 25px; }
      .form-control { background-color: #0d1117; border: 1px solid #30363d; color: #c9d1d9; border-radius: 6px; }
      .form-control:focus { background-color: #0d1117; border-color: #58a6ff; color: #c9d1d9; box-shadow: 0 0 0 3px rgba(88,166,255,0.2); }
      .login-err { background-color: rgba(218,54,51,0.15); border: 1px solid #da3633; color: #f85149; padding: 8px 12px; border-radius: 6px; font-size: 12px; margin-bottom: 15px; text-align: center; }
    ")),
    tags$script(HTML("
      // Client-side collapse/expand for the right-panel stacked groups.
      $(document).on('click', '.collap-header', function() {
        $(this).next('.collap-body').slideToggle(120);
        $(this).find('.chev').toggleClass('rot');
        setTimeout(function() { $(window).trigger('resize'); }, 160);
      });

      // Rock-solid interaction-aware scroll tracking
      window.userScrollState = window.userScrollState || {};
      window.logWasAtBottom = true;
      var isUserInteracting = false;
      var userInteractTimer = null;

      function flagUserInteraction() {
        isUserInteracting = true;
        clearTimeout(userInteractTimer);
        userInteractTimer = setTimeout(function() {
          isUserInteracting = false;
        }, 600);
      }

      // Listen for genuine user input events (wheel, touch, drag, keys)
      window.addEventListener('wheel', flagUserInteraction, { passive: true, capture: true });
      window.addEventListener('touchmove', flagUserInteraction, { passive: true, capture: true });
      window.addEventListener('pointerdown', flagUserInteraction, { passive: true, capture: true });
      window.addEventListener('mousedown', flagUserInteraction, { passive: true, capture: true });
      window.addEventListener('keydown', flagUserInteraction, { passive: true, capture: true });

      // Record scroll ONLY when initiated by user interaction
      $(document).on('scroll', '.preserve-scroll, .telemetry-scroll, .panel-card-right, .scroll-container, .terminal-log', function(e) {
        var id = this.id || 'open_trades_container';
        
        if (isUserInteracting) {
          window.userScrollState[id] = this.scrollTop;
          if (id === 'terminal_log') {
            var threshold = 40;
            window.logWasAtBottom = (this.scrollHeight - this.clientHeight <= this.scrollTop + threshold);
          }
        } else {
          // If a DOM update silently dropped scroll to 0 while user had a saved scroll > 0, restore it!
          var saved = window.userScrollState[id];
          if (saved && saved > 0 && this.scrollTop !== saved) {
            var elem = this;
            requestAnimationFrame(function() {
              elem.scrollTop = saved;
            });
          }
        }
      });

      // Restore all scroll containers to their recorded positions
      function restoreAllScrolls() {
        if (isUserInteracting) return;
        $('.preserve-scroll, .telemetry-scroll, .panel-card-right, .scroll-container, .terminal-log').each(function() {
          var id = this.id || 'open_trades_container';
          if (id === 'terminal_log') {
            if (window.logWasAtBottom) {
              this.scrollTop = this.scrollHeight;
            } else if (window.userScrollState[id] !== undefined) {
              this.scrollTop = window.userScrollState[id];
            }
          } else if (window.userScrollState[id] !== undefined && window.userScrollState[id] > 0) {
            var target = window.userScrollState[id];
            if (this.scrollTop !== target) {
              this.scrollTop = target;
            }
          }
        });
      }

      // Re-apply on Shiny update events
      $(document).on('shiny:value shiny:idle shiny:recalculating', function() {
        restoreAllScrolls();
        setTimeout(restoreAllScrolls, 10);
        setTimeout(restoreAllScrolls, 50);
      });

      // MutationObserver on scroll areas to catch individual card DOM updates
      var scrollObserver = new MutationObserver(function() {
        restoreAllScrolls();
      });

      function bindObservers() {
        $('.preserve-scroll, .telemetry-scroll, .panel-card-right, .scroll-container, .terminal-log').each(function() {
          try {
            scrollObserver.observe(this, { childList: true, subtree: true });
          } catch(e) {}
        });
      }

      $(document).ready(bindObservers);
      $(document).on('shiny:connected shiny:idle', bindObservers);
    "))
  ),

  # Login View
  conditionalPanel(
    condition = "!output.is_authenticated",
    div(class = "login-container",
      div(class = "login-card",
        div(class = "login-title", icon("shield-alt"), " CBQT TERMINAL ACCESS"),
        div(class = "login-sub", "Hit-Monitor & Intraday Risk Suite"),

        uiOutput("login_error_ui"),
        div(style = "margin-bottom: 15px;",
          tags$label(style = "font-size: 12px; color: #8b949e; margin-bottom: 4px;", "Username"),
          textInput("auth_user", NULL, placeholder = "Enter username", width = "100%")
        ),
        div(style = "margin-bottom: 25px;",
          tags$label(style = "font-size: 12px; color: #8b949e; margin-bottom: 4px;", "Password"),
          passwordInput("auth_pass", NULL, placeholder = "Enter password", width = "100%")
        ),
        actionButton("btn_login", "Authenticate Session", class = "btn btn-custom", style = "width: 100%; padding: 10px; font-size: 14px;", icon = icon("lock"))
      )
    )
  ),

  # Authenticated Dashboard View
  conditionalPanel(
    condition = "output.is_authenticated",
    # Top Bar / Controls & Summary
    fluidRow(
      column(12,
        div(class = "header-card",
          fluidRow(
            column(3,
              div(style = "display: flex; align-items: center; gap: 10px;",
                div(
                  h4(style = "margin-top: 0; margin-bottom: 2px; color: #58a6ff; font-weight: bold;", "CBQT HIT-MONITOR"),
                  p(style = "margin: 0; font-size: 12px; color: #8b949e;", "CGG Standing Strategy & Telemetry")
                )
              )
            ),
            column(2,
              selectInput("date", "Session Date:", choices = available_dates, selected = default_date, width = "100%")
            ),
            column(2,
              selectInput("strategy", "Strategy:", choices = c("CGG (Standing 51°)" = "cgg", "Legacy Asymmetric (40/7)" = "asymmetric"), selected = "cgg", width = "100%")
            ),
            column(2,
              selectInput("start_time", "Start Time:", choices = c("09:30" = "9:30", "09:38" = "9:38"), selected = "9:30", width = "100%")
            ),
            column(3,
              div(style = "display: flex; gap: 8px; align-items: center; justify-content: flex-end; height: 58px;",
                uiOutput("active_user_ui"),
                actionButton("btn_logout", "Sign Out", class = "btn-logout", icon = icon("sign-out-alt"))
              )
            )
          ),
          hr(style = "border-color: #30363d; margin-top: 10px; margin-bottom: 10px;"),
          fluidRow(
            column(2, div(class = "metric-box", div(class = "metric-lbl", "Active Open"), uiOutput("m_open"))),
            column(2, div(class = "metric-box", div(class = "metric-lbl", "Closed Trades"), uiOutput("m_closed"))),
            column(2, div(class = "metric-box", div(class = "metric-lbl", "Gross Realized"), uiOutput("m_gross"))),
            column(2, div(class = "metric-box", div(class = "metric-lbl", "Win Rate"), uiOutput("m_winrate"))),
            column(2, div(class = "metric-box", div(class = "metric-lbl", "Regime / IRF"), uiOutput("m_irf"))),
            column(2, div(class = "metric-box", div(class = "metric-lbl", "Latest Feed Tick"), uiOutput("m_tick")))
          )
        )
      )
    ),

    # Dual Panel Layout
    fluidRow(
      # Left Panel: Signal Log & Completed Trades (unchanged)
      column(6,
        div(class = "panel-card",
          div(style = "display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;",
            h5(style = "margin: 0; color: #58a6ff; font-weight: bold;", icon("list-ul"), " SIGNALS & COMPLETED TRADES"),
            radioButtons("left_view", NULL, choices = c("Console Log" = "log", "Trade Table" = "table"), selected = "log", inline = TRUE)
          ),
          conditionalPanel(
            condition = "input.left_view == 'log'",
            div(class = "terminal-log preserve-scroll", id = "terminal_log",
              uiOutput("left_panel_log", inline = TRUE)
            )
          ),
          conditionalPanel(
            condition = "input.left_view == 'table'",
            div(class = "scroll-container preserve-scroll", id = "trade_table_container",
              uiOutput("left_panel_table")
            )
          )
        )
      ),

      # Right Panel: two collapsible stacked groups
      column(6,
        div(class = "panel-card-right preserve-scroll", id = "right_panel_main_scroll",

          # Group 1: Open Trades Telemetry (own scroll region)
          div(class = "collap-header",
            h5(class = "collap-title", style = "color: #3fb950;",
               icon("chart-line"), " OPEN TRADES TELEMETRY & RISK MONITOR"),
            div(
              span(style = "font-size: 12px; color: #8b949e; margin-right: 10px;", "Amplitude · Kalman · Hazard · Gate"),
              span(class = "chev", icon("chevron-down"))
            )
          ),
          div(class = "collap-body",
            div(class = "telemetry-scroll preserve-scroll", id = "open_trades_container",
              uiOutput("right_panel_content")
            )
          ),

          # Group 2: Live Boundary Chart ("real-time slide 13")
          div(class = "collap-header", style = "margin-top: 10px;",
            h5(class = "collap-title", style = "color: #58a6ff;",
               icon("wave-square"), " LIVE BOUNDARY CHART — PRICE vs 51° FORECAST BANDS"),
            span(class = "chev", icon("chevron-down"))
          ),
          div(class = "collap-body",
            div(style = "display: flex; align-items: center; gap: 10px; margin-bottom: 6px;",
              span(style = "font-size: 12px; color: #8b949e;", "REIT:"),
              div(style = "width: 170px;",
                selectInput("chart_sym", NULL, choices = NULL, width = "100%")
              ),
              span(style = "font-size: 11px; color: #8b949e;",
                   "Dollar price · red = sell boundary (Phi), blue = buy boundary (Plo) · green/red dots = BUY/SELL prints")
            ),
            div(class = "chart-frame",
              plotOutput("boundary_plot", height = "430px")
            )
          )
        )
      )
    )
  )
)

server <- function(input, output, session) {

  # ----------------------------------------------------------------------------
  # ANTI-PULSE GATE: each output re-renders ONLY when its own payload changes.
  # req(FALSE, cancelOutput = TRUE) leaves the existing DOM untouched, so ticks
  # that do not alter a given output cause zero DOM churn for it.
  # ----------------------------------------------------------------------------
  render_cache <- new.env(parent = emptyenv())
  payload_hash <- function(x) as.character(openssl::sha256(serialize(x, NULL, version = 2)))
  gate <- function(key, payload) {
    h <- payload_hash(payload)
    if (identical(render_cache[[key]], h)) req(FALSE, cancelOutput = TRUE)
    render_cache[[key]] <- h
    invisible(TRUE)
  }

  # Authentication State
  is_auth <- reactiveVal(FALSE)
  current_user <- reactiveVal("")
  login_error <- reactiveVal(FALSE)

  output$is_authenticated <- reactive({
    is_auth()
  })
  outputOptions(output, "is_authenticated", suspendWhenHidden = FALSE)

  observeEvent(input$btn_login, {
    user_in <- tolower(trimws(input$auth_user))
    pass_in <- input$auth_pass

    if (nzchar(user_in) && nzchar(pass_in) && user_in %in% names(AUTH_HASHES)) {
      pass_hash <- as.character(openssl::sha256(pass_in))
      if (identical(pass_hash, AUTH_HASHES[[user_in]])) {
        current_user(user_in)
        is_auth(TRUE)
        login_error(FALSE)
        return()
      }
    }
    login_error(TRUE)
  })

  observeEvent(input$btn_logout, {
    is_auth(FALSE)
    current_user("")
    login_error(FALSE)
  })

  output$login_error_ui <- renderUI({
    if (login_error()) {
      div(class = "login-err", icon("exclamation-circle"), " Invalid username or password.")
    } else NULL
  })

  output$active_user_ui <- renderUI({
    req(is_auth())
    span(class = "user-badge", icon("user"), paste0(" ", current_user()))
  })

  # Dynamically discover available session dates and default to newest
  observe({
    dates_now <- tryCatch({
      dir("/data/local3/prices/") %>%
        str_extract("^[0-9]{4}-[0-9]{2}-[0-9]{2}") %>%
        compact() %>%
        unique() %>%
        sort(decreasing = TRUE)
    }, error = function(e) as.character(Sys.Date()))

    req(length(dates_now) > 0)
    current_sel <- isolate(input$date)
    if (is.null(current_sel) || !nzchar(current_sel) || !current_sel %in% dates_now) {
      updateSelectInput(session, "date", choices = dates_now, selected = dates_now[1])
    } else {
      updateSelectInput(session, "date", choices = dates_now, selected = current_sel)
    }
  })

  # Populate the boundary-chart REIT dropdown based on selected date
  observeEvent(input$date, {
    req(input$date)
    sel_d <- tryCatch(as.Date(input$date), error = function(e) Sys.Date())
    book_choices <- if (!is.na(sel_d) && sel_d >= as.Date("2026-08-24")) {
      sort(setdiff(c(revr::BOOK18, "X_VMRK"), c("MF_AVB", "MF_EQR")))
    } else {
      sort(revr::BOOK18)
    }
    curr_sym <- isolate(input$chart_sym)
    sel_sym <- if (!is.null(curr_sym) && curr_sym %in% book_choices) {
      curr_sym
    } else if ("RT_SPG" %in% book_choices) {
      "RT_SPG"
    } else {
      book_choices[1]
    }
    updateSelectInput(session, "chart_sym", choices = book_choices, selected = sel_sym)
  }, priority = 10)

  # Reactive file path to watch
  file_path_rx <- reactive({
    sel_date <- if (!is.null(input$date) && nzchar(input$date)) {
      input$date
    } else {
      dates_now <- dir("/data/local3/prices/") %>% str_extract("^[0-9]{4}-[0-9]{2}-[0-9]{2}") %>% compact() %>% sort(decreasing = TRUE)
      if (length(dates_now) > 0) dates_now[1] else as.character(Sys.Date())
    }
    sprintf("/data/local3/prices/%s.csv", sel_date)
  })

  # Reactive file reader that checks modification time every 500ms
  file_mod_time <- reactiveFileReader(
    intervalMillis = 500,
    session = session,
    filePath = file_path_rx,
    readFunc = function(path) {
      if (file.exists(path)) {
        info <- file.info(path)
        paste(info$mtime, info$size)
      } else {
        as.character(Sys.time())
      }
    }
  )

  # Fetch current state from monitor_state engine
  state_data <- reactive({
    req(is_auth())

    sel_date <- if (!is.null(input$date) && nzchar(input$date)) {
      input$date
    } else {
      dates_now <- dir("/data/local3/prices/") %>% str_extract("^[0-9]{4}-[0-9]{2}-[0-9]{2}") %>% compact() %>% sort(decreasing = TRUE)
      if (length(dates_now) > 0) dates_now[1] else as.character(Sys.Date())
    }
    sel_strat <- if (!is.null(input$strategy)) input$strategy else "cgg"
    sel_start <- if (!is.null(input$start_time)) input$start_time else "9:30"

    file_mod_time()

    cgg_monitor_state(
      date = sel_date,
      strategy = sel_strat,
      percentile = if (identical(sel_strat, "cgg")) 51 else c(40, 7),
      start = sel_start,
      stop = "16:15",
      gate_fraction = 0.40,
      gate_ttl_min = 63,
      cut_1230_bps = -75,
      ramp_end = "13:30"
    )
  })

  # Summary Metrics (digest-gated: only the box whose value changed re-renders)
  output$m_open <- renderUI({
    n <- state_data()$summary$n_open
    gate("m_open", n)
    div(class = "metric-val", style = if (n > 0) "color: #58a6ff;" else "color: #8b949e;", n)
  })

  output$m_closed <- renderUI({
    n <- state_data()$summary$n_closed
    gate("m_closed", n)
    div(class = "metric-val", n)
  })

  output$m_gross <- renderUI({
    g <- state_data()$summary$gross_bps
    gate("m_gross", g)
    col <- if (g > 0) "#3fb950" else if (g < 0) "#f85149" else "#8b949e"
    div(class = "metric-val", style = paste0("color: ", col, ";"), sprintf("%+.1f bps", g))
  })

  output$m_winrate <- renderUI({
    w <- state_data()$summary$win_rate_pct
    gate("m_winrate", w)
    div(class = "metric-val", style = "color: #e3b341;", sprintf("%.1f%%", w))
  })

  output$m_irf <- renderUI({
    turb <- state_data()$summary$is_turbulent
    gate("m_irf", turb)
    if (turb) {
      span(class = "tag-cut", "TURBULENT (12:45 STOP)")
    } else {
      span(style = "color: #3fb950; font-weight: bold;", "CALM (16:15 STOP)")
    }
  })

  output$m_tick <- renderUI({
    tk <- state_data()$summary$latest_time
    gate("m_tick", tk)
    div(class = "metric-val", style = "font-size: 16px; color: #8b949e;", tk)
  })

  # Left Panel Rendering (digest-gated; DOM untouched unless a new line/row lands)
  output$left_panel_log <- renderUI({
    st <- state_data()
    log_txt <- if (length(st$log_lines) > 0) {
      paste(st$log_lines, collapse = "\n")
    } else {
      "Waiting for intraday price feed and signal triggers..."
    }
    gate("left_log", log_txt)
    tags$span(style = "white-space: pre-wrap;", log_txt)
  })

  output$left_panel_table <- renderUI({
    n <- nrow(state_data()$closed_trades)
    gate("left_table_shell", n == 0)
    if (n == 0) {
      div(style = "text-align: center; color: #8b949e; padding: 40px;", "No completed trades yet today.")
    } else {
      tableOutput("trade_table")
    }
  })

  output$trade_table <- renderTable({
    df <- state_data()$closed_trades
    if (nrow(df) == 0) return(NULL)
    gate("trade_table", df)
    df %>%
      mutate(
        ret_bps = sprintf("%+.1f", ret_bps),
        entry_px = sprintf("%.2f", entry_px),
        exit_px = sprintf("%.2f", exit_px)
      ) %>%
      rename(
        Symbol = symbol, Side = side, `Entry Time` = entry_time, `Exit Time` = exit_time,
        `Entry Px` = entry_px, `Exit Px` = exit_px, `PnL (bps)` = ret_bps, Reason = exit_reason
      )
  }, striped = TRUE, hover = TRUE, bordered = TRUE, width = "100%")

  # ----------------------------------------------------------------------------
  # Right Panel Group 1: per-symbol card outputs.
  # The container re-renders ONLY when the SET of open symbols changes; each
  # card re-renders ONLY when its own telemetry changes. A tick that moves one
  # name repaints one card — every other DOM node (and your scroll) stays put.
  # ----------------------------------------------------------------------------
  build_card <- function(pos) {
    is_long <- (pos$side == "LONG")
    badge_cls <- if (is_long) "badge-long" else "badge-short"
    pnl_cls <- if (pos$mtm_bps >= 0) "pnl-pos" else "pnl-neg"
    pnl_str <- sprintf("%+.1f bps", pos$mtm_bps)

    gate_pct <- min(100, max(0, (pos$age_min / pos$gate_ttl_min) * 100))
    gate_bar_col <- if (pos$age_min <= pos$gate_ttl_min) "#238636" else "#8b949e"

    cut_banner <- if (pos$cut_1230_alert) {
      div(style = "background-color: #8e1519; color: white; padding: 4px 8px; border-radius: 4px; margin-top: 6px; font-size: 11px; font-weight: bold;",
        icon("exclamation-triangle"), " 12:30 CUT WARNING: MTM <= -75 bps"
      )
    } else NULL

    div(class = "pos-card",
      div(class = "pos-header",
        div(
          span(style = "font-size: 16px; font-weight: bold; color: white; margin-right: 8px;", pos$symbol),
          span(class = badge_cls, pos$side),
          span(style = "font-size: 11px; color: #8b949e; margin-left: 8px;", paste("Entry:", pos$entry_time))
        ),
        div(
          span(style = "font-size: 12px; color: #8b949e; margin-right: 10px;", sprintf("Px: %.2f -> %.2f", pos$entry_px, pos$curr_px)),
          span(class = pnl_cls, pnl_str)
        )
      ),

      div(
        div(style = "display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 2px;",
          span(style = "color: #58a6ff; font-weight: 600;", pos$gate_status),
          span(style = "color: #8b949e;", sprintf("Age: %.1fm / %dm", pos$age_min, pos$gate_ttl_min))
        ),
        div(class = "progress",
          div(class = "progress-bar", style = sprintf("width: %d%%; background-color: %s;", round(gate_pct), gate_bar_col))
        ),
        div(style = "display: flex; justify-content: space-between; font-size: 11px; color: #8b949e;",
          span(sprintf("Opp 51° (Phi/Plo): %.2f (Dist: %.1f bps)", pos$opp_bnd, pos$dist_bps)),
          span(sprintf("Frac: %.1f%% (Ask: +%.1f bps)", pos$fraction_pct, pos$target_ask_bps))
        )
      ),

      div(class = "telemetry-grid",
        div(style = "background-color: #161b22; padding: 8px; border-radius: 4px;",
          div(class = "sub-metric", "Excursion Amplitude (σ):"),
          div(class = "sub-val", sprintf("%.1f bps · %s", pos$sigma_amp_bps, pos$amp_tercile)),
          div(class = "sub-metric", style = "margin-top: 4px;", "Kalman Local State (m̂):"),
          div(class = "sub-val",
            sprintf("%+.1f · ", pos$m_hat),
            span(class = if (pos$kalman_class == "TRENDING STATE") "tag-state" else "tag-spike", pos$kalman_class)
          )
        ),
        div(style = "background-color: #161b22; padding: 8px; border-radius: 4px;",
          div(class = "sub-metric", "Pr(Δ0 Recovery before Close):"),
          div(class = "sub-val", style = if (pos$p_recovery_pct > 60) "color: #3fb950;" else "color: #f85149;",
            sprintf("%.1f%% (π: %.1f%%)", pos$p_recovery_pct, pos$pi_pathological_pct)
          ),
          div(class = "sub-metric", style = "margin-top: 4px;", "Expected Timing / Exit:"),
          div(class = "sub-val",
            sprintf("Touch: %s · Ride: %+.1f bps", pos$iqr_touch_min, pos$expected_exit_bps)
          )
        )
      ),
      cut_banner
    )
  }

  # Container: one placeholder uiOutput per open symbol (stable across ticks)
  output$right_panel_content <- renderUI({
    syms <- names(state_data()$open_trades)
    gate("right_container", syms)

    if (length(syms) == 0) {
      return(div(
        style = "text-align: center; color: #8b949e; padding: 60px 20px;",
        icon("satellite-dish", style = "font-size: 40px; color: #30363d; margin-bottom: 15px;"),
        h4(style = "color: #8b949e;", "No Open Exposures"),
        p("All trades are currently paired, gated, or awaiting entry conditions.")
      ))
    }
    tagList(lapply(syms, function(s) uiOutput(paste0("card_", s))))
  })

  # Register each per-symbol card renderer exactly once
  cards_registered <- new.env(parent = emptyenv())
  observe({
    syms <- names(state_data()$open_trades)
    for (s in syms) {
      if (is.null(cards_registered[[s]])) {
        cards_registered[[s]] <- TRUE
        local({
          sym <- s
          output[[paste0("card_", sym)]] <- renderUI({
            pos <- state_data()$open_trades[[sym]]
            if (is.null(pos)) return(NULL)
            gate(paste0("card_", sym), pos)
            build_card(pos)
          })
        })
      }
    }
  })

  # ----------------------------------------------------------------------------
  # Right Panel Group 2: Live Boundary Chart ("real-time slide 13").
  # White exhibit frame per the deck: gray observed price (indexed to open),
  # red Phi (sell boundary), blue Plo (buy boundary), shaded band between,
  # green BUY prints / red SELL prints, hollow marker on the latest print.
  # ----------------------------------------------------------------------------
  # Resolve cgg_boundary_series from the revr namespace even if the NAMESPACE
  # export lags a reinstall; NULL (function absent) degrades to a plot message.
  boundary_series_fn <- tryCatch(
    get("cgg_boundary_series", envir = asNamespace("revr")),
    error = function(e) NULL
  )

  boundary_data <- reactive({
    req(is_auth(), input$chart_sym, nzchar(input$chart_sym), !is.null(boundary_series_fn))

    sel_date <- if (!is.null(input$date) && nzchar(input$date)) input$date else default_date
    sel_strat <- if (!is.null(input$strategy)) input$strategy else "cgg"
    sel_start <- if (!is.null(input$start_time)) input$start_time else "9:30"

    file_mod_time()

    tryCatch(
      boundary_series_fn(
        date = sel_date,
        symbol = input$chart_sym,
        percentile = if (identical(sel_strat, "cgg")) 51 else 40,
        start = sel_start,
        stop = "16:15"
      ),
      error = function(e) NULL
    )
  })

  output$boundary_plot <- renderPlot({
    validate(need(!is.null(boundary_series_fn),
      "cgg_boundary_series() not found in the installed revr package.\nCopy R/boundary_series.R into the package and reinstall revr (R CMD INSTALL), then restart the app."))
    bd <- boundary_data()
    req(!is.null(bd), nrow(bd$series) > 0)
    gate("boundary_plot", list(bd$symbol, bd$date, bd$series, bd$execs))

    sr <- bd$series[!is.na(bd$series$px), , drop = FALSE]
    req(nrow(sr) > 0)

    start_sec <- min(bd$series$sec)
    stop_sec <- 16*3600 + 15*60
    hour_brks <- seq(10*3600, 16*3600, by = 3600)

    last_row <- sr[nrow(sr), , drop = FALSE]
    last_hms <- sprintf("%02d:%02d:%02d",
                        last_row$sec %/% 3600, (last_row$sec %% 3600) %/% 60,
                        round(last_row$sec %% 60))

    p <- ggplot(sr, aes(x = sec)) +
      geom_ribbon(aes(ymin = plo_px, ymax = phi_px), fill = "grey93") +
      geom_line(aes(y = phi_px), color = "#b30000", linewidth = 0.7) +
      geom_line(aes(y = plo_px), color = "#1f4e9c", linewidth = 0.7) +
      geom_line(aes(y = px), color = "grey45", linewidth = 0.45)

    if (nrow(bd$execs) > 0) {
      p <- p + geom_point(
        data = bd$execs,
        aes(x = sec, y = px),
        fill = ifelse(bd$execs$print == "BUY", "#1a7f37", "#b30000"),
        color = "black", shape = 21, size = 3.4, stroke = 0.4
      )
    }

    p +
      geom_point(data = last_row, aes(x = sec, y = px),
                 shape = 21, size = 2.6, stroke = 0.9, color = "black", fill = "white") +
      scale_x_continuous(limits = c(start_sec, stop_sec),
                         breaks = hour_brks,
                         labels = sprintf("%d:00", hour_brks %/% 3600),
                         expand = expansion(mult = c(0.01, 0.01))) +
      scale_y_continuous(labels = function(x) sprintf("$%.2f", x)) +
      labs(
        title = sprintf("%s — %s (live through %s)", bd$symbol, bd$date, last_hms),
        subtitle = "Observed dollar price vs 51° Gumbel forecast boundaries",
        x = NULL, y = "Price ($)"
      ) +
      theme_minimal(base_size = 12) +
      theme(
        plot.title = element_text(face = "bold", size = 13),
        plot.subtitle = element_text(color = "grey35", size = 10),
        panel.grid.minor = element_blank(),
        panel.grid.major = element_line(color = "grey88", linewidth = 0.3),
        axis.text = element_text(color = "grey25"),
        plot.background = element_rect(fill = "white", color = NA),
        panel.background = element_rect(fill = "white", color = NA)
      )
  }, res = 96)
}

shinyApp(ui = ui, server = server)
