diff --git a/lib/cortex_status_web/components/layouts/root.html.heex b/lib/cortex_status_web/components/layouts/root.html.heex
index 2a43f0a..6cb01a8 100644
--- a/lib/cortex_status_web/components/layouts/root.html.heex
+++ b/lib/cortex_status_web/components/layouts/root.html.heex
@@ -1236,6 +1236,237 @@
text-align: center;
color: var(--text-muted);
}
+
+ /* Session detail modal */
+ .modal-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ padding: 2rem;
+ animation: fadeIn 0.15s ease-out;
+ }
+
+ @keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+ }
+
+ .modal-content {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ width: 100%;
+ max-width: 640px;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ animation: slideUp 0.2s ease-out;
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
+ }
+
+ @keyframes slideUp {
+ from { transform: translateY(20px); opacity: 0; }
+ to { transform: translateY(0); opacity: 1; }
+ }
+
+ .modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1.25rem 1.5rem;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ }
+
+ .modal-title-row {
+ display: flex;
+ align-items: center;
+ gap: 0.6rem;
+ }
+
+ .modal-close {
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ font-size: 1.5rem;
+ cursor: pointer;
+ padding: 0.25rem 0.5rem;
+ border-radius: 6px;
+ line-height: 1;
+ transition: all 0.15s;
+ }
+
+ .modal-close:hover {
+ color: var(--text);
+ background: var(--surface-hover);
+ }
+
+ .modal-status-badge {
+ font-size: 0.7rem;
+ font-family: var(--mono);
+ padding: 0.15rem 0.5rem;
+ border-radius: 4px;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ }
+
+ .modal-status-completed { color: var(--green); background: var(--green-bg); }
+ .modal-status-active { color: var(--blue); background: var(--blue-bg); }
+ .modal-status-unknown { color: var(--text-muted); background: rgba(136, 144, 164, 0.1); }
+
+ .modal-body {
+ padding: 1.5rem;
+ overflow-y: auto;
+ flex: 1;
+ }
+
+ .modal-summary {
+ font-size: 1.1rem;
+ font-weight: 500;
+ line-height: 1.5;
+ margin-bottom: 1.25rem;
+ }
+
+ .modal-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem 2rem;
+ margin-bottom: 1.25rem;
+ padding-bottom: 1.25rem;
+ border-bottom: 1px solid var(--border);
+ }
+
+ .meta-item {
+ display: flex;
+ flex-direction: column;
+ gap: 0.2rem;
+ }
+
+ .meta-label {
+ font-size: 0.7rem;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ }
+
+ .meta-value {
+ font-size: 0.85rem;
+ color: var(--text);
+ }
+
+ .meta-value.mono {
+ font-family: var(--mono);
+ font-size: 0.8rem;
+ }
+
+ .modal-completion {
+ margin-bottom: 1.25rem;
+ padding: 1rem;
+ background: var(--green-bg);
+ border: 1px solid rgba(52, 211, 153, 0.2);
+ border-radius: 8px;
+ }
+
+ .completion-label {
+ font-size: 0.7rem;
+ color: var(--green);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ display: block;
+ margin-bottom: 0.4rem;
+ }
+
+ .completion-text {
+ font-size: 0.9rem;
+ line-height: 1.5;
+ color: var(--text);
+ }
+
+ .modal-logs-section {
+ margin-top: 0.5rem;
+ }
+
+ .logs-header {
+ font-size: 0.9rem;
+ font-weight: 600;
+ margin-bottom: 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ .logs-count {
+ font-size: 0.75rem;
+ font-weight: 400;
+ color: var(--text-muted);
+ font-family: var(--mono);
+ }
+
+ .logs-loading {
+ padding: 1.5rem;
+ text-align: center;
+ color: var(--text-muted);
+ font-style: italic;
+ }
+
+ .logs-empty {
+ padding: 1.5rem;
+ text-align: center;
+ color: var(--text-muted);
+ }
+
+ .logs-timeline {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ border-left: 2px solid var(--border);
+ margin-left: 0.5rem;
+ padding-left: 1rem;
+ }
+
+ .log-entry {
+ display: flex;
+ gap: 0.75rem;
+ padding: 0.5rem 0;
+ position: relative;
+ }
+
+ .log-entry::before {
+ content: '';
+ position: absolute;
+ left: -1.35rem;
+ top: 0.85rem;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--border);
+ border: 2px solid var(--surface);
+ }
+
+ .log-time {
+ font-size: 0.75rem;
+ font-family: var(--mono);
+ color: var(--text-muted);
+ flex-shrink: 0;
+ min-width: 3rem;
+ padding-top: 0.1rem;
+ }
+
+ .log-text {
+ font-size: 0.85rem;
+ line-height: 1.5;
+ color: var(--text);
+ }
+
+ /* Make session rows clickable */
+ .engram-session-row {
+ cursor: pointer;
+ }
diff --git a/lib/cortex_status_web/live/status_live.ex b/lib/cortex_status_web/live/status_live.ex
index d40a107..66e248b 100644
--- a/lib/cortex_status_web/live/status_live.ex
+++ b/lib/cortex_status_web/live/status_live.ex
@@ -13,7 +13,14 @@ defmodule CortexStatusWeb.StatusLive do
status = CortexStatus.Services.Monitor.get_status()
- {:ok, assign(socket, status: status, page_title: "Cortex Status")}
+ {:ok,
+ assign(socket,
+ status: status,
+ page_title: "Cortex Status",
+ selected_session: nil,
+ session_logs: [],
+ modal_loading: false
+ )}
end
@impl true
@@ -21,6 +28,42 @@ defmodule CortexStatusWeb.StatusLive do
{:noreply, assign(socket, status: new_status)}
end
+ @impl true
+ def handle_event("select_session", %{"id" => session_id}, socket) do
+ # Find the session in our cached data
+ sessions = socket.assigns.status.engram.sessions
+ session = Enum.find(sessions, fn s -> get_val(s, "id", nil) == session_id end)
+
+ if session do
+ # Fetch logs async
+ send(self(), {:fetch_logs, session_id})
+ {:noreply, assign(socket, selected_session: session, session_logs: [], modal_loading: true)}
+ else
+ {:noreply, socket}
+ end
+ end
+
+ def handle_event("close_modal", _params, socket) do
+ {:noreply, assign(socket, selected_session: nil, session_logs: [], modal_loading: false)}
+ end
+
+ @impl true
+ def handle_info({:fetch_logs, session_id}, socket) do
+ config = Application.get_env(:cortex_status, :services, [])
+ url = Keyword.get(config, :symbiont_url, "http://127.0.0.1:8111")
+
+ logs =
+ case Req.get("#{url}/engram/sessions/#{session_id}/logs?limit=100", receive_timeout: 5_000) do
+ {:ok, %{status: 200, body: %{"logs" => logs}}} when is_list(logs) -> logs
+ _ -> []
+ end
+
+ # Sort logs oldest-first (they come newest-first from the API)
+ logs = Enum.reverse(logs)
+
+ {:noreply, assign(socket, session_logs: logs, modal_loading: false)}
+ end
+
@impl true
def render(assigns) do
~H"""
@@ -62,6 +105,14 @@ defmodule CortexStatusWeb.StatusLive do
<.engram_section engram={@status.engram} />
+ <%= if @selected_session do %>
+ <.session_modal
+ session={@selected_session}
+ logs={@session_logs}
+ loading={@modal_loading}
+ />
+ <% end %>
+