From bf489977b0b6afeb54c7f03861af49f24a015a02 Mon Sep 17 00:00:00 2001 From: David Astor Date: Thu, 5 Mar 2026 12:07:18 -0500 Subject: [PATCH 1/3] initial ui update --- WORKFLOW.md | 17 +- lib/symphony_elixir/config.ex | 18 +- lib/symphony_elixir/orchestrator.ex | 22 +++ .../live/dashboard_live.ex | 165 ++++++++++++------ lib/symphony_elixir_web/presenter.ex | 17 +- priv/static/dashboard.css | 126 ++++++++++++- test/symphony_elixir/extensions_test.exs | 3 + 7 files changed, 302 insertions(+), 66 deletions(-) diff --git a/WORKFLOW.md b/WORKFLOW.md index d102b62fe..ab66c4ad7 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -1,18 +1,11 @@ --- tracker: kind: linear - project_slug: "symphony-0c79b11b75ea" - active_states: - - Todo - - In Progress - - Merging - - Rework - terminal_states: - - Closed - - Cancelled - - Canceled - - Duplicate - - Done + team_key: "RVR" + labels: ["symphony"] + assignee: "me" + active_states: ["Todo", "In Progress", "Code Review", "On Staging"] + terminal_states: ["Done", "Canceled"] polling: interval_ms: 5000 workspace: diff --git a/lib/symphony_elixir/config.ex b/lib/symphony_elixir/config.ex index 839903ec2..eba72ea78 100644 --- a/lib/symphony_elixir/config.ex +++ b/lib/symphony_elixir/config.ex @@ -370,7 +370,23 @@ defmodule SymphonyElixir.Config do port _ -> - get_in(validated_workflow_options(), [:server, :port]) + case get_in(validated_workflow_options(), [:server, :port]) do + port when is_integer(port) and port >= 0 -> port + _ -> env_server_port() + end + end + end + + defp env_server_port do + case System.get_env("SYMPHONY_SERVER_PORT") do + nil -> + nil + + val -> + case Integer.parse(val) do + {port, _} when port >= 0 -> port + _ -> nil + end end end diff --git a/lib/symphony_elixir/orchestrator.ex b/lib/symphony_elixir/orchestrator.ex index cffd7520b..bb88fdd4e 100644 --- a/lib/symphony_elixir/orchestrator.ex +++ b/lib/symphony_elixir/orchestrator.ex @@ -110,6 +110,7 @@ defmodule SymphonyElixir.Orchestrator do |> complete_issue(issue_id) |> schedule_issue_retry(issue_id, 1, %{ identifier: running_entry.identifier, + issue_url: running_entry.issue.url, delay_type: :continuation }) @@ -120,6 +121,7 @@ defmodule SymphonyElixir.Orchestrator do schedule_issue_retry(state, issue_id, next_attempt, %{ identifier: running_entry.identifier, + issue_url: running_entry.issue.url, error: "agent exited: #{inspect(reason)}" }) end @@ -398,6 +400,7 @@ defmodule SymphonyElixir.Orchestrator do |> terminate_running_issue(issue_id, false) |> schedule_issue_retry(issue_id, next_attempt, %{ identifier: identifier, + issue_url: running_entry[:issue] && running_entry.issue.url, error: "stalled for #{elapsed_ms}ms without codex activity" }) else @@ -654,6 +657,7 @@ defmodule SymphonyElixir.Orchestrator do last_codex_message: nil, last_codex_timestamp: nil, last_codex_event: nil, + codex_event_log: [], codex_app_server_pid: nil, codex_input_tokens: 0, codex_output_tokens: 0, @@ -679,6 +683,7 @@ defmodule SymphonyElixir.Orchestrator do schedule_issue_retry(state, issue.id, next_attempt, %{ identifier: issue.identifier, + issue_url: issue.url, error: "failed to spawn agent: #{inspect(reason)}" }) end @@ -720,6 +725,7 @@ defmodule SymphonyElixir.Orchestrator do old_timer = Map.get(previous_retry, :timer_ref) due_at_ms = System.monotonic_time(:millisecond) + delay_ms identifier = pick_retry_identifier(issue_id, previous_retry, metadata) + issue_url = pick_retry_issue_url(previous_retry, metadata) error = pick_retry_error(previous_retry, metadata) if is_reference(old_timer) do @@ -740,6 +746,7 @@ defmodule SymphonyElixir.Orchestrator do timer_ref: timer_ref, due_at_ms: due_at_ms, identifier: identifier, + issue_url: issue_url, error: error }) } @@ -846,6 +853,7 @@ defmodule SymphonyElixir.Orchestrator do attempt + 1, Map.merge(metadata, %{ identifier: issue.identifier, + issue_url: issue.url, error: "no available orchestrator slots" }) )} @@ -883,6 +891,10 @@ defmodule SymphonyElixir.Orchestrator do metadata[:identifier] || Map.get(previous_retry, :identifier) || issue_id end + defp pick_retry_issue_url(previous_retry, metadata) do + metadata[:issue_url] || Map.get(previous_retry, :issue_url) + end + defp pick_retry_error(previous_retry, metadata) do metadata[:error] || Map.get(previous_retry, :error) end @@ -969,11 +981,13 @@ defmodule SymphonyElixir.Orchestrator do codex_input_tokens: metadata.codex_input_tokens, codex_output_tokens: metadata.codex_output_tokens, codex_total_tokens: metadata.codex_total_tokens, + codex_event_log: Map.get(metadata, :codex_event_log, []), turn_count: Map.get(metadata, :turn_count, 0), started_at: metadata.started_at, last_codex_timestamp: metadata.last_codex_timestamp, last_codex_message: metadata.last_codex_message, last_codex_event: metadata.last_codex_event, + issue_url: metadata.issue.url, runtime_seconds: running_seconds(metadata.started_at, now) } end) @@ -986,6 +1000,7 @@ defmodule SymphonyElixir.Orchestrator do attempt: attempt, due_in_ms: max(0, due_at_ms - now_ms), identifier: Map.get(retry, :identifier), + issue_url: Map.get(retry, :issue_url), error: Map.get(retry, :error) } end) @@ -1022,6 +1037,8 @@ defmodule SymphonyElixir.Orchestrator do }, state} end + @max_event_log_entries 200 + defp integrate_codex_update(running_entry, %{event: event, timestamp: timestamp} = update) do token_delta = extract_token_delta(running_entry, update) codex_input_tokens = Map.get(running_entry, :codex_input_tokens, 0) @@ -1033,12 +1050,17 @@ defmodule SymphonyElixir.Orchestrator do last_reported_total = Map.get(running_entry, :codex_last_reported_total_tokens, 0) turn_count = Map.get(running_entry, :turn_count, 0) + log_entry = %{event: event, timestamp: timestamp, message: summarize_codex_update(update)} + event_log = Map.get(running_entry, :codex_event_log, []) + updated_log = Enum.take([log_entry | event_log], @max_event_log_entries) + { Map.merge(running_entry, %{ last_codex_timestamp: timestamp, last_codex_message: summarize_codex_update(update), session_id: session_id_for_update(running_entry.session_id, update), last_codex_event: event, + codex_event_log: updated_log, codex_app_server_pid: codex_app_server_pid_for_update(codex_app_server_pid, update), codex_input_tokens: codex_input_tokens + token_delta.input_tokens, codex_output_tokens: codex_output_tokens + token_delta.output_tokens, diff --git a/lib/symphony_elixir_web/live/dashboard_live.ex b/lib/symphony_elixir_web/live/dashboard_live.ex index a30631c11..637b86de5 100644 --- a/lib/symphony_elixir_web/live/dashboard_live.ex +++ b/lib/symphony_elixir_web/live/dashboard_live.ex @@ -14,6 +14,7 @@ defmodule SymphonyElixirWeb.DashboardLive do socket |> assign(:payload, load_payload()) |> assign(:now, DateTime.utc_now()) + |> assign(:expanded, MapSet.new()) if connected?(socket) do :ok = ObservabilityPubSub.subscribe() @@ -37,6 +38,23 @@ defmodule SymphonyElixirWeb.DashboardLive do |> assign(:now, DateTime.utc_now())} end + @impl true + def handle_event("noop", _params, socket), do: {:noreply, socket} + + @impl true + def handle_event("toggle_row", %{"id" => issue_id}, socket) do + expanded = socket.assigns.expanded + + expanded = + if MapSet.member?(expanded, issue_id) do + MapSet.delete(expanded, issue_id) + else + MapSet.put(expanded, issue_id) + end + + {:noreply, assign(socket, :expanded, expanded)} + end + @impl true def render(assigns) do ~H""" @@ -149,57 +167,86 @@ defmodule SymphonyElixirWeb.DashboardLive do - - -
- <%= entry.issue_identifier %> - JSON details -
- - - - <%= entry.state %> - - - -
- <%= if entry.session_id do %> - - <% else %> - n/a - <% end %> -
- - <%= format_runtime_and_turns(entry.started_at, entry.turn_count, @now) %> - -
- <%= entry.last_message || to_string(entry.last_event || "n/a") %> - - <%= entry.last_event || "n/a" %> - <%= if entry.last_event_at do %> - · <%= entry.last_event_at %> + <%= for entry <- @payload.running do %> + + +
+ <%= if MapSet.member?(@expanded, entry.issue_id), do: "\u25BE", else: "\u25B8" %> + <%= if entry.issue_url do %> + <%= entry.issue_identifier %> + <% else %> + <%= entry.issue_identifier %> <% end %> + JSON +
+ + + + <%= entry.state %> -
- - -
- Total: <%= format_int(entry.tokens.total_tokens) %> - In <%= format_int(entry.tokens.input_tokens) %> / Out <%= format_int(entry.tokens.output_tokens) %> -
- - + + +
+ <%= if entry.session_id do %> + + <% else %> + n/a + <% end %> +
+ + <%= format_runtime_and_turns(entry.started_at, entry.turn_count, @now) %> + +
+ <%= entry.last_message || to_string(entry.last_event || "n/a") %> + + <%= entry.last_event || "n/a" %> + <%= if entry.last_event_at do %> + · <%= entry.last_event_at %> + <% end %> + +
+ + +
+ Total: <%= format_int(entry.tokens.total_tokens) %> + In <%= format_int(entry.tokens.input_tokens) %> / Out <%= format_int(entry.tokens.output_tokens) %> +
+ + + <%= if MapSet.member?(@expanded, entry.issue_id) do %> + + +
+

Session event log

+ <%= if entry.event_log == [] do %> +

No events recorded yet.

+ <% else %> +
+ <%= for log_entry <- entry.event_log do %> +
+ <%= log_entry.timestamp || "n/a" %> + <%= log_entry.event || "unknown" %> + <%= log_entry.message || "" %> +
+ <% end %> +
+ <% end %> +
+ + + <% end %> + <% end %> @@ -231,8 +278,12 @@ defmodule SymphonyElixirWeb.DashboardLive do
- <%= entry.issue_identifier %> - JSON details + <%= if entry.issue_url do %> + <%= entry.issue_identifier %> + <% else %> + <%= entry.issue_identifier %> + <% end %> + JSON
<%= entry.attempt %> @@ -321,6 +372,18 @@ defmodule SymphonyElixirWeb.DashboardLive do end end + defp event_badge_class(event) do + base = "event-badge" + normalized = event |> to_string() |> String.downcase() + + cond do + String.contains?(normalized, ["failed", "error", "malformed"]) -> "#{base} event-badge-danger" + String.contains?(normalized, ["started", "completed", "approved"]) -> "#{base} event-badge-success" + String.contains?(normalized, ["cancelled", "input_required"]) -> "#{base} event-badge-warning" + true -> base + end + end + defp schedule_runtime_tick do Process.send_after(self(), :runtime_tick, @runtime_tick_ms) end diff --git a/lib/symphony_elixir_web/presenter.ex b/lib/symphony_elixir_web/presenter.ex index 34eb1e664..c6ca7507e 100644 --- a/lib/symphony_elixir_web/presenter.ex +++ b/lib/symphony_elixir_web/presenter.ex @@ -75,7 +75,7 @@ defmodule SymphonyElixirWeb.Presenter do running: running && running_issue_payload(running), retry: retry && retry_issue_payload(retry), logs: %{ - codex_session_logs: [] + codex_session_logs: (running && format_event_log(Map.get(running, :codex_event_log, []))) || [] }, recent_events: (running && recent_events_payload(running)) || [], last_error: retry && retry.error, @@ -98,6 +98,7 @@ defmodule SymphonyElixirWeb.Presenter do %{ issue_id: entry.issue_id, issue_identifier: entry.identifier, + issue_url: Map.get(entry, :issue_url), state: entry.state, session_id: entry.session_id, turn_count: Map.get(entry, :turn_count, 0), @@ -105,6 +106,7 @@ defmodule SymphonyElixirWeb.Presenter do last_message: summarize_message(entry.last_codex_message), started_at: iso8601(entry.started_at), last_event_at: iso8601(entry.last_codex_timestamp), + event_log: format_event_log(Map.get(entry, :codex_event_log, [])), tokens: %{ input_tokens: entry.codex_input_tokens, output_tokens: entry.codex_output_tokens, @@ -117,6 +119,7 @@ defmodule SymphonyElixirWeb.Presenter do %{ issue_id: entry.issue_id, issue_identifier: entry.identifier, + issue_url: Map.get(entry, :issue_url), attempt: entry.attempt, due_at: due_at_iso8601(entry.due_in_ms), error: entry.error @@ -159,6 +162,18 @@ defmodule SymphonyElixirWeb.Presenter do |> Enum.reject(&is_nil(&1.at)) end + defp format_event_log(log) when is_list(log) do + Enum.map(log, fn entry -> + %{ + event: entry.event, + timestamp: iso8601(entry.timestamp), + message: summarize_message(entry.message) + } + end) + end + + defp format_event_log(_), do: [] + defp summarize_message(nil), do: nil defp summarize_message(message), do: StatusDashboard.humanize_codex_message(message) diff --git a/priv/static/dashboard.css b/priv/static/dashboard.css index bc191c0ca..46b5b73cb 100644 --- a/priv/static/dashboard.css +++ b/priv/static/dashboard.css @@ -326,7 +326,6 @@ pre, font-size: 0.94rem; } -.issue-stack, .session-stack, .detail-stack, .token-stack { @@ -431,6 +430,131 @@ pre, color: var(--danger); } +.expandable-row { + cursor: pointer; + transition: background 100ms ease; +} + +.expandable-row:hover { + background: var(--page-soft); +} + +.row-chevron { + display: inline-block; + width: 1em; + color: var(--muted); + font-size: 0.82rem; + flex-shrink: 0; +} + +.issue-stack { + display: flex; + align-items: baseline; + gap: 0.4rem; + flex-wrap: wrap; + min-width: 0; +} + +a.issue-id { + font-weight: 600; + letter-spacing: -0.01em; + color: var(--ink); + text-decoration: none; +} + +a.issue-id:hover { + color: var(--accent); +} + +.expanded-detail-row td { + padding: 0; + border-top: none; +} + +.event-log-panel { + padding: 0.75rem 1rem 1rem; + background: var(--page-soft); + border-top: 1px solid var(--line); + border-bottom: 1px solid var(--line); +} + +.event-log-title { + margin: 0 0 0.6rem; + font-size: 0.82rem; + font-weight: 600; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.event-log-list { + display: grid; + gap: 0; + max-height: 400px; + overflow-y: auto; +} + +.event-log-entry { + display: flex; + align-items: baseline; + gap: 0.6rem; + padding: 0.35rem 0; + border-bottom: 1px solid var(--line); + font-size: 0.88rem; + line-height: 1.45; +} + +.event-log-entry:last-child { + border-bottom: none; +} + +.event-log-time { + flex-shrink: 0; + color: var(--muted); + font-size: 0.8rem; +} + +.event-badge { + display: inline-flex; + align-items: center; + flex-shrink: 0; + padding: 0.15rem 0.5rem; + border-radius: 999px; + border: 1px solid var(--line); + background: var(--card-muted); + color: var(--ink); + font-size: 0.75rem; + font-weight: 600; + line-height: 1; + white-space: nowrap; +} + +.event-badge-success { + background: var(--accent-soft); + border-color: rgba(16, 163, 127, 0.18); + color: var(--accent-ink); +} + +.event-badge-danger { + background: var(--danger-soft); + border-color: #f6d3cf; + color: var(--danger); +} + +.event-badge-warning { + background: #fff7e8; + border-color: #f1d8a6; + color: #8a5a00; +} + +.event-log-message { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--ink); +} + @media (max-width: 860px) { .app-shell { padding: 1rem 0.85rem 2rem; diff --git a/test/symphony_elixir/extensions_test.exs b/test/symphony_elixir/extensions_test.exs index 59c8d0580..98def9a5e 100644 --- a/test/symphony_elixir/extensions_test.exs +++ b/test/symphony_elixir/extensions_test.exs @@ -347,6 +347,7 @@ defmodule SymphonyElixir.ExtensionsTest do %{ "issue_id" => "issue-http", "issue_identifier" => "MT-HTTP", + "issue_url" => nil, "state" => "In Progress", "session_id" => "thread-http", "turn_count" => 7, @@ -354,6 +355,7 @@ defmodule SymphonyElixir.ExtensionsTest do "last_message" => "rendered", "started_at" => state_payload["running"] |> List.first() |> Map.fetch!("started_at"), "last_event_at" => nil, + "event_log" => [], "tokens" => %{"input_tokens" => 4, "output_tokens" => 8, "total_tokens" => 12} } ], @@ -361,6 +363,7 @@ defmodule SymphonyElixir.ExtensionsTest do %{ "issue_id" => "issue-retry", "issue_identifier" => "MT-RETRY", + "issue_url" => nil, "attempt" => 2, "due_at" => state_payload["retrying"] |> List.first() |> Map.fetch!("due_at"), "error" => "boom" From 93f782cbff0f79d3600b3c741e562c73cbf77709 Mon Sep 17 00:00:00 2001 From: David Astor Date: Thu, 5 Mar 2026 12:44:59 -0500 Subject: [PATCH 2/3] Redesign dashboard UI with Alto-inspired dark theme Replace the light theme with a dark design language matching Alto: black backgrounds, gold accent gradient, Inter typeface, solid dark card surfaces with subtle borders, and opacity-based interactions. --- lib/symphony_elixir_web/components/layouts.ex | 5 +- priv/static/dashboard.css | 431 ++++++++++-------- 2 files changed, 248 insertions(+), 188 deletions(-) diff --git a/lib/symphony_elixir_web/components/layouts.ex b/lib/symphony_elixir_web/components/layouts.ex index afac13e3f..6e29b5934 100644 --- a/lib/symphony_elixir_web/components/layouts.ex +++ b/lib/symphony_elixir_web/components/layouts.ex @@ -16,7 +16,10 @@ defmodule SymphonyElixirWeb.Layouts do - Symphony Observability + Symphony + + + diff --git a/priv/static/dashboard.css b/priv/static/dashboard.css index 46b5b73cb..5883941e0 100644 --- a/priv/static/dashboard.css +++ b/priv/static/dashboard.css @@ -1,21 +1,43 @@ :root { - color-scheme: light; - --page: #f7f7f8; - --page-soft: #fbfbfc; - --page-deep: #ececf1; - --card: rgba(255, 255, 255, 0.94); - --card-muted: #f3f4f6; - --ink: #202123; - --muted: #6e6e80; - --line: #ececf1; - --line-strong: #d9d9e3; - --accent: #10a37f; - --accent-ink: #0f513f; - --accent-soft: #e8faf4; - --danger: #b42318; - --danger-soft: #fef3f2; - --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.05); - --shadow-lg: 0 20px 50px rgba(15, 23, 42, 0.08); + color-scheme: dark; + + /* Surfaces */ + --page: #000000; + --page-soft: #0A0A0A; + --page-deep: #131313; + --card: #1D1D1D; + --card-muted: #262626; + + /* Text */ + --ink: #E4E4E4; + --secondary: #B8B8B8; + --muted: #9A9A9A; + + /* Borders */ + --line: #262626; + --line-strong: #393939; + + /* Accent – Alto gold */ + --accent: #C5A063; + --accent-soft: rgba(197, 160, 99, 0.12); + --accent-ink: #C5A063; + --accent-gradient: linear-gradient(111deg, #d4b583 14.74%, #987b4a 80.2%); + + /* Semantic */ + --danger: #EF4444; + --danger-soft: rgba(239, 68, 68, 0.10); + --success: #22C55E; + --success-soft: rgba(34, 197, 94, 0.10); + --warning: #F59E0B; + --warning-soft: rgba(245, 158, 11, 0.10); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-lg: 0 20px 50px rgba(0, 0, 0, 0.4); + + /* Typography */ + --font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: "SF Mono", "SFMono-Regular", Consolas, "Liberation Mono", monospace; } * { @@ -29,83 +51,80 @@ html { body { margin: 0; min-height: 100vh; - background: - radial-gradient(circle at top, rgba(16, 163, 127, 0.12) 0%, rgba(16, 163, 127, 0) 30%), - linear-gradient(180deg, var(--page-soft) 0%, var(--page) 24%, #f3f4f6 100%); + background: var(--page); color: var(--ink); - font-family: "Sohne", "SF Pro Text", "Helvetica Neue", "Segoe UI", sans-serif; + font-family: var(--font-sans); + font-size: 14px; line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } a { color: var(--ink); text-decoration: none; - transition: color 140ms ease; + transition: opacity 140ms ease; } a:hover { - color: var(--accent); + opacity: 0.8; } button { appearance: none; border: 1px solid var(--accent); - background: var(--accent); - color: white; - border-radius: 999px; - padding: 0.72rem 1.08rem; + background: var(--accent-gradient); + color: #000000; + border-radius: 9999px; + padding: 8px 16px; cursor: pointer; - font: inherit; + font-family: var(--font-sans); + font-size: 14px; font-weight: 600; letter-spacing: -0.01em; - box-shadow: 0 8px 20px rgba(16, 163, 127, 0.18); - transition: - transform 140ms ease, - box-shadow 140ms ease, - background 140ms ease, - border-color 140ms ease; + box-shadow: none; + transition: opacity 140ms ease; } button:hover { - transform: translateY(-1px); - box-shadow: 0 12px 24px rgba(16, 163, 127, 0.22); + opacity: 0.9; + transform: none; + box-shadow: none; } button.secondary { background: var(--card); color: var(--ink); border-color: var(--line-strong); - box-shadow: var(--shadow-sm); + box-shadow: none; } button.secondary:hover { - box-shadow: 0 6px 16px rgba(15, 23, 42, 0.08); + opacity: 0.9; + box-shadow: none; } .subtle-button { appearance: none; border: 1px solid var(--line-strong); - background: rgba(255, 255, 255, 0.72); + background: var(--card-muted); color: var(--muted); - border-radius: 999px; - padding: 0.34rem 0.72rem; + border-radius: 9999px; + padding: 4px 12px; cursor: pointer; - font: inherit; - font-size: 0.82rem; + font-family: var(--font-sans); + font-size: 12px; font-weight: 600; letter-spacing: 0.01em; box-shadow: none; - transition: - background 140ms ease, - border-color 140ms ease, - color 140ms ease; + transition: opacity 140ms ease, background 140ms ease, color 140ms ease; } .subtle-button:hover { transform: none; box-shadow: none; - background: white; - border-color: var(--muted); + background: var(--line-strong); + border-color: var(--line-strong); color: var(--ink); } @@ -118,7 +137,7 @@ pre { code, pre, .mono { - font-family: "Sohne Mono", "SFMono-Regular", "SF Mono", Consolas, "Liberation Mono", monospace; + font-family: var(--font-mono); } .mono, @@ -127,6 +146,8 @@ pre, font-feature-settings: "tnum" 1, "zero" 1; } +/* ── Layout ────────────────────────────────────────────────────── */ + .app-shell { max-width: 1280px; margin: 0 auto; @@ -135,55 +156,60 @@ pre, .dashboard-shell { display: grid; - gap: 1rem; + gap: 16px; } +/* ── Shared card base ──────────────────────────────────────────── */ + .hero-card, .section-card, .metric-card, .error-card { background: var(--card); - border: 1px solid rgba(217, 217, 227, 0.82); - box-shadow: var(--shadow-sm); - backdrop-filter: blur(18px); + border: 1px solid var(--line); + box-shadow: none; } +/* ── Hero ───────────────────────────────────────────────────────── */ + .hero-card { - border-radius: 28px; - padding: clamp(1.25rem, 3vw, 2rem); - box-shadow: var(--shadow-lg); + border-radius: 16px; + padding: clamp(20px, 3vw, 32px); } .hero-grid { display: grid; grid-template-columns: minmax(0, 1fr) auto; - gap: 1.25rem; + gap: 20px; align-items: start; } .eyebrow { margin: 0; - color: var(--muted); + color: var(--accent); text-transform: uppercase; letter-spacing: 0.08em; - font-size: 0.76rem; + font-size: 12px; font-weight: 600; } .hero-title { - margin: 0.35rem 0 0; - font-size: clamp(2rem, 4vw, 3.3rem); + margin: 4px 0 0; + font-size: clamp(32px, 4vw, 48px); line-height: 0.98; letter-spacing: -0.04em; + color: var(--ink); } .hero-copy { - margin: 0.75rem 0 0; + margin: 12px 0 0; max-width: 46rem; - color: var(--muted); - font-size: 1rem; + color: var(--secondary); + font-size: 16px; } +/* ── Status badges ─────────────────────────────────────────────── */ + .status-stack { display: grid; justify-items: end; @@ -194,35 +220,35 @@ pre, .status-badge { display: inline-flex; align-items: center; - gap: 0.45rem; - min-height: 2rem; - padding: 0.35rem 0.78rem; - border-radius: 999px; - border: 1px solid var(--line); + gap: 8px; + min-height: 32px; + padding: 6px 12px; + border-radius: 9999px; + border: 1px solid var(--line-strong); background: var(--card-muted); color: var(--muted); - font-size: 0.82rem; - font-weight: 700; + font-size: 12px; + font-weight: 600; letter-spacing: 0.01em; } .status-badge-dot { - width: 0.52rem; - height: 0.52rem; - border-radius: 999px; + width: 8px; + height: 8px; + border-radius: 9999px; background: currentColor; opacity: 0.9; } .status-badge-live { display: none; - background: var(--accent-soft); - border-color: rgba(16, 163, 127, 0.18); - color: var(--accent-ink); + background: var(--success-soft); + border-color: rgba(34, 197, 94, 0.20); + color: var(--success); } .status-badge-offline { - background: #f5f5f7; + background: var(--card-muted); border-color: var(--line-strong); color: var(--muted); } @@ -235,41 +261,46 @@ pre, display: none; } +/* ── Metric cards ──────────────────────────────────────────────── */ + .metric-grid { display: grid; - gap: 0.85rem; + gap: 12px; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); } .metric-card { - border-radius: 22px; - padding: 1rem 1.05rem 1.1rem; + border-radius: 12px; + padding: 16px; } .metric-label { margin: 0; color: var(--muted); - font-size: 0.82rem; - font-weight: 600; + font-size: 12px; + font-weight: 500; letter-spacing: 0.01em; } .metric-value { - margin: 0.35rem 0 0; - font-size: clamp(1.6rem, 2vw, 2.1rem); + margin: 8px 0 0; + font-size: clamp(24px, 2vw, 32px); line-height: 1.05; letter-spacing: -0.03em; + color: var(--ink); } .metric-detail { - margin: 0.45rem 0 0; + margin: 8px 0 0; color: var(--muted); - font-size: 0.88rem; + font-size: 14px; } +/* ── Section cards ─────────────────────────────────────────────── */ + .section-card { - border-radius: 24px; - padding: 1.15rem; + border-radius: 16px; + padding: 24px; } .section-header { @@ -282,20 +313,24 @@ pre, .section-title { margin: 0; - font-size: 1.08rem; + font-size: 18px; + font-weight: 600; line-height: 1.2; letter-spacing: -0.02em; + color: var(--ink); } .section-copy { - margin: 0.35rem 0 0; + margin: 4px 0 0; color: var(--muted); - font-size: 0.94rem; + font-size: 14px; } +/* ── Data tables ───────────────────────────────────────────────── */ + .table-wrap { overflow-x: auto; - margin-top: 1rem; + margin-top: 16px; } .data-table { @@ -310,27 +345,30 @@ pre, } .data-table th { - padding: 0 0.5rem 0.75rem 0; + padding: 0 8px 12px 0; text-align: left; color: var(--muted); - font-size: 0.78rem; - font-weight: 600; + font-size: 12px; + font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em; } .data-table td { - padding: 0.9rem 0.5rem 0.9rem 0; + padding: 12px 8px 12px 0; border-top: 1px solid var(--line); vertical-align: top; - font-size: 0.94rem; + font-size: 14px; + color: var(--ink); } +/* ── Stacks ────────────────────────────────────────────────────── */ + .session-stack, .detail-stack, .token-stack { display: grid; - gap: 0.24rem; + gap: 4px; min-width: 0; } @@ -350,38 +388,42 @@ pre, white-space: nowrap; } +/* ── State badges ──────────────────────────────────────────────── */ + .state-badge { display: inline-flex; align-items: center; - min-height: 1.85rem; - padding: 0.3rem 0.68rem; - border-radius: 999px; - border: 1px solid var(--line); + min-height: 28px; + padding: 4px 12px; + border-radius: 9999px; + border: 1px solid var(--line-strong); background: var(--card-muted); - color: var(--ink); - font-size: 0.8rem; + color: var(--secondary); + font-size: 12px; font-weight: 600; line-height: 1; } .state-badge-active { - background: var(--accent-soft); - border-color: rgba(16, 163, 127, 0.18); - color: var(--accent-ink); + background: var(--success-soft); + border-color: rgba(34, 197, 94, 0.20); + color: var(--success); } .state-badge-warning { - background: #fff7e8; - border-color: #f1d8a6; - color: #8a5a00; + background: var(--warning-soft); + border-color: rgba(245, 158, 11, 0.20); + color: var(--warning); } .state-badge-danger { background: var(--danger-soft); - border-color: #f6d3cf; + border-color: rgba(239, 68, 68, 0.20); color: var(--danger); } +/* ── Issue links ───────────────────────────────────────────────── */ + .issue-id { font-weight: 600; letter-spacing: -0.01em; @@ -389,46 +431,34 @@ pre, .issue-link { color: var(--muted); - font-size: 0.86rem; + font-size: 14px; } .muted { color: var(--muted); } -.code-panel { - margin-top: 1rem; - padding: 1rem; - border-radius: 18px; - background: #f5f5f7; - border: 1px solid var(--line); - color: #353740; - font-size: 0.9rem; -} - -.empty-state { - margin: 1rem 0 0; - color: var(--muted); +.issue-stack { + display: flex; + align-items: baseline; + gap: 8px; + flex-wrap: wrap; + min-width: 0; } -.error-card { - border-radius: 24px; - padding: 1.25rem; - background: linear-gradient(180deg, #fff8f7 0%, var(--danger-soft) 100%); - border-color: #f6d3cf; +a.issue-id { + font-weight: 600; + letter-spacing: -0.01em; + color: var(--ink); + text-decoration: none; } -.error-title { - margin: 0; - color: var(--danger); - font-size: 1.15rem; - letter-spacing: -0.02em; +a.issue-id:hover { + color: var(--accent); + opacity: 1; } -.error-copy { - margin: 0.45rem 0 0; - color: var(--danger); -} +/* ── Expandable rows ───────────────────────────────────────────── */ .expandable-row { cursor: pointer; @@ -436,51 +466,74 @@ pre, } .expandable-row:hover { - background: var(--page-soft); + background: var(--page-deep); } .row-chevron { display: inline-block; width: 1em; color: var(--muted); - font-size: 0.82rem; + font-size: 12px; flex-shrink: 0; } -.issue-stack { - display: flex; - align-items: baseline; - gap: 0.4rem; - flex-wrap: wrap; - min-width: 0; +.expanded-detail-row td { + padding: 0; + border-top: none; } -a.issue-id { - font-weight: 600; - letter-spacing: -0.01em; - color: var(--ink); - text-decoration: none; +/* ── Code panel ────────────────────────────────────────────────── */ + +.code-panel { + margin-top: 16px; + padding: 16px; + border-radius: 12px; + background: var(--page-deep); + border: 1px solid var(--line); + color: var(--secondary); + font-size: 14px; } -a.issue-id:hover { - color: var(--accent); +/* ── Empty state ───────────────────────────────────────────────── */ + +.empty-state { + margin: 16px 0 0; + color: var(--muted); } -.expanded-detail-row td { - padding: 0; - border-top: none; +/* ── Error card ────────────────────────────────────────────────── */ + +.error-card { + border-radius: 16px; + padding: 24px; + background: var(--danger-soft); + border-color: rgba(239, 68, 68, 0.25); +} + +.error-title { + margin: 0; + color: var(--danger); + font-size: 18px; + letter-spacing: -0.02em; } +.error-copy { + margin: 8px 0 0; + color: var(--danger); +} + +/* ── Event log panel ───────────────────────────────────────────── */ + .event-log-panel { - padding: 0.75rem 1rem 1rem; + padding: 12px 16px 16px; background: var(--page-soft); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } .event-log-title { - margin: 0 0 0.6rem; - font-size: 0.82rem; + margin: 0 0 12px; + font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; @@ -497,10 +550,10 @@ a.issue-id:hover { .event-log-entry { display: flex; align-items: baseline; - gap: 0.6rem; - padding: 0.35rem 0; + gap: 12px; + padding: 6px 0; border-bottom: 1px solid var(--line); - font-size: 0.88rem; + font-size: 14px; line-height: 1.45; } @@ -511,49 +564,53 @@ a.issue-id:hover { .event-log-time { flex-shrink: 0; color: var(--muted); - font-size: 0.8rem; + font-size: 12px; } +.event-log-message { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--secondary); +} + +/* ── Event badges ──────────────────────────────────────────────── */ + .event-badge { display: inline-flex; align-items: center; flex-shrink: 0; - padding: 0.15rem 0.5rem; - border-radius: 999px; - border: 1px solid var(--line); + padding: 2px 8px; + border-radius: 9999px; + border: 1px solid var(--line-strong); background: var(--card-muted); - color: var(--ink); - font-size: 0.75rem; + color: var(--secondary); + font-size: 12px; font-weight: 600; line-height: 1; white-space: nowrap; } .event-badge-success { - background: var(--accent-soft); - border-color: rgba(16, 163, 127, 0.18); - color: var(--accent-ink); + background: var(--success-soft); + border-color: rgba(34, 197, 94, 0.20); + color: var(--success); } .event-badge-danger { background: var(--danger-soft); - border-color: #f6d3cf; + border-color: rgba(239, 68, 68, 0.20); color: var(--danger); } .event-badge-warning { - background: #fff7e8; - border-color: #f1d8a6; - color: #8a5a00; + background: var(--warning-soft); + border-color: rgba(245, 158, 11, 0.20); + color: var(--warning); } -.event-log-message { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: var(--ink); -} +/* ── Responsive ────────────────────────────────────────────────── */ @media (max-width: 860px) { .app-shell { @@ -581,7 +638,7 @@ a.issue-id:hover { .section-card, .hero-card, .error-card { - border-radius: 20px; - padding: 1rem; + border-radius: 12px; + padding: 16px; } } From c8546d51b5c0239aef7a0a27933f6ab3e5d2284e Mon Sep 17 00:00:00 2001 From: David Astor Date: Thu, 5 Mar 2026 12:46:45 -0500 Subject: [PATCH 3/3] Update documentation for server port and dispatch state configuration Add SYMPHONY_SERVER_PORT environment variable documentation and port priority order. Update WORKFLOW.md to include dispatch_states configuration. --- README.md | 2 ++ WORKFLOW.md | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 53f25c39d..ca66743e2 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,8 @@ codex: - If `WORKFLOW.md` is missing or has invalid YAML, startup and scheduling are halted until fixed. - `server.port` or CLI `--port` enables the optional Phoenix LiveView dashboard and JSON API at `/`, `/api/v1/state`, `/api/v1/`, and `/api/v1/refresh`. +- The `SYMPHONY_SERVER_PORT` environment variable can also set the server port. Priority order: + CLI `--port` > WORKFLOW.md `server.port` > `SYMPHONY_SERVER_PORT` env var. ### Issue filtering diff --git a/WORKFLOW.md b/WORKFLOW.md index ab66c4ad7..96af0aeb0 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -4,7 +4,8 @@ tracker: team_key: "RVR" labels: ["symphony"] assignee: "me" - active_states: ["Todo", "In Progress", "Code Review", "On Staging"] + dispatch_states: "Todo, In Progress" + active_states: "Todo, In Progress, Code Review, On Staging" terminal_states: ["Done", "Canceled"] polling: interval_ms: 5000