Fix the staging build, and settle the sidebar footer

cee26598af17 · AtlantisPleb · · parent 0ebf4198fd34

Fix the staging build, and settle the sidebar footer

The staging deploy failed at the Codex install step with `TARGETARCH:
parameter not set`. That arg is provided automatically by BuildKit, and Cloud
Build's classic docker builder does not set it, so `set -eu` turned it into a
failed build. The consumer falls back to the builder's own architecture, which
resolves on both paths and leaves the checksum guarding the result.

Leaderboard moves to the footer: it is somewhere you look, not somewhere you
work, which is the same category as the docs and the component library.

The footer rows are sidebar rows now rather than an approximation of them.
They had drifted to 36px against the nav's 32px and a smaller type size, which
read as looser spacing below the rule than above it. Same height, same insets,
same gap, same type; the only difference left is which side of the rule they
sit on.

The computers page loses its indigo radial wash. It was the only place in the
application where a page tinted itself, and once the palette went monochrome
it read as a rendering artefact rather than as design.

The composer loses the rule and the space above it. The card already reads as
its own control against the transcript, so a border plus 16px stacked a second
edge under the one the card draws.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149rBWy7br1Z7bbz9NrQhEr
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified Dockerfile
  • modified assets/css/openagents.css
  • modified lib/openagents/admin.ex
  • modified lib/openagents_web/components/layouts.ex
  • modified test/openagents_web/live/chat_live_test.exs

Diff

5 files changed, +90 -22

Dockerfile modified +5 -1

@@ -110,6 +110,10 @@ CMD ["mix", "run", "--no-compile", "--no-start", "ops/forge/build-worker.exs"]

110 110
# the compiled release and other runtime necessities
111 111
FROM ${RUNNER_IMAGE} AS final
112 112
113
# BuildKit sets TARGETARCH automatically; Cloud Build's classic docker builder
114
# does not, and the `set -eu` below turns an unset value into a failed build.
115
# The consumer falls back to the builder's own architecture, so both paths
116
# resolve and the checksum still guards the result.
113 117
ARG TARGETARCH
114 118
ARG CODEX_VERSION
115 119
ARG DEBIAN_SNAPSHOT

@@ -128,7 +132,7 @@ RUN sed -i \

128 132
  && rm -rf /var/lib/apt/lists/*
129 133
130 134
RUN set -eu; \
131
  case "${TARGETARCH}" in \
135
  case "${TARGETARCH:-$(dpkg --print-architecture)}" in \
132 136
    amd64) codex_arch=x86_64; checksum=0246e2e773834e07f0fb5249ed6ebad12e4591e608f8c7bb97dd6a9690544c36 ;; \
133 137
    arm64) codex_arch=aarch64; checksum=eb677c80f666b1ab8b4b1d083b66e8d614b1281d960bb6f9fd8ca98f58b38b90 ;; \
134 138
    *) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
assets/css/openagents.css modified +21 -13

@@ -250,9 +250,10 @@

250 250
    padding: clamp(24px, 4vw, 44px) clamp(16px, 4vw, 36px) 56px;
251 251
    overflow-x: hidden;
252 252
    overflow-y: auto;
253
    background:
254
      radial-gradient(circle at 18% 0%, color-mix(in oklab, var(--accent) 8%, transparent), transparent 34%),
255
      var(--ink-surface);
253
    /* Flat. The indigo radial wash in this corner was the only place in the
254
       application where a page tinted itself, and it read as a rendering
255
       artefact rather than as design once the palette went monochrome. */
256
    background: var(--ink-surface);
256 257
    scrollbar-color: var(--icon-faint) transparent;
257 258
    scrollbar-width: thin;
258 259
  }

@@ -2529,27 +2530,32 @@

2529 2530
    align-items: stretch;
2530 2531
    margin-top: auto;
2531 2532
    border-top: 1px solid var(--line);
2532
    /* Block padding only. The inline inset comes from the link, so a footer
2533
       link lines up with the nav rows above it instead of sitting at its own
2534
       indent. */
2535
    padding: 12px 0;
2533
    /* Block padding only -- the inline inset comes from the link, so a footer
2534
       link lines up with the nav rows above it. The 2px matches
2535
       `.sidebar-nav`'s padding-block so both columns breathe identically. */
2536
    padding: 8px 0 10px;
2536 2537
  }
2537 2538
2539
  /* A footer link is a sidebar row. Same height, same insets, same gap, same
2540
     type -- the only difference is that these are secondary destinations, so
2541
     they sit under a rule. Matching .sidebar-row by hand had drifted: 36px
2542
     against the row's 32px, which read as looser spacing in the footer than in
2543
     the nav above it. */
2538 2544
  .sidebar-footer__link {
2539 2545
    display: flex;
2540 2546
    align-items: center;
2541 2547
    justify-content: flex-start;
2542 2548
    gap: 8px;
2543
    min-height: 36px;
2544
    /* Same 12px + 8px as .sidebar-row, so the two columns share one left edge. */
2549
    min-height: 32px;
2545 2550
    margin-inline: 12px;
2546 2551
    padding-inline: 8px;
2552
    border-radius: 6px;
2547 2553
    color: var(--text-muted);
2548
    font-size: 0.8125rem;
2554
    font-size: 0.875rem;
2549 2555
    font-weight: 500;
2550 2556
    line-height: 1.25rem;
2551 2557
    text-decoration: none;
2552
    transition: color 150ms var(--ease);
2558
    transition: color 150ms var(--ease), background 150ms var(--ease);
2553 2559
  }
2554 2560
2555 2561
  .sidebar-footer__link .icon {

@@ -2792,9 +2798,11 @@

2792 2798
2793 2799
  .composer-region {
2794 2800
    flex: none;
2795
    border-top: 1px solid var(--line);
2796 2801
    background: var(--ink-void);
2797
    padding: 16px 24px 20px;
2802
    /* No rule and no space above it. The composer already reads as its own
2803
       control against the transcript, and a border plus 16px stacked a second
2804
       edge under the one the card draws. */
2805
    padding: 0 24px 20px;
2798 2806
  }
2799 2807
2800 2808
  .composer-eyebrow {
lib/openagents/admin.ex modified +57

@@ -18,7 +18,9 @@ defmodule OpenAgents.Admin do

18 18
  alias OpenAgents.Accounts.User
19 19
  alias OpenAgents.Admin.Call
20 20
  alias OpenAgents.Conversations.Conversation
21
  alias OpenAgents.Conversations.Message
21 22
  alias OpenAgents.Conversations.Visitor
23
  alias OpenAgents.Issues.Issue
22 24
  alias OpenAgents.Repo
23 25
  alias OpenAgents.Voice.Recording
24 26
  alias OpenAgents.Voice.Session

@@ -27,6 +29,61 @@ defmodule OpenAgents.Admin do

27 29
  @default_limit 50
28 30
  @maximum_limit 200
29 31
32
  @doc """
33
  Every account, newest first, with the activity each has on the site.
34
35
  One query, not one per account: a list of N accounts that costs N+1 queries
36
  is the kind of thing that is fine with three accounts and unusable with
37
  three thousand. Counts come from left joins so an account with no activity
38
  is listed with zeroes rather than dropped.
39
40
  `joined_at` is the account row's own creation, not the GitHub account's; it
41
  is when this site first saw them.
42
  """
43
  @spec list_accounts(keyword()) :: [map()]
44
  def list_accounts(options \\ []) do
45
    limit = options |> Keyword.get(:limit) |> bound_limit()
46
    offset = max(Keyword.get(options, :offset, 0), 0)
47
48
    messages =
49
      from(m in Message,
50
        join: c in Conversation,
51
        on: c.id == m.conversation_id,
52
        group_by: c.visitor_id,
53
        select: %{visitor_id: c.visitor_id, count: count(m.id), last_at: max(m.inserted_at)}
54
      )
55
56
    from(u in User,
57
      left_join: v in Visitor,
58
      on: v.user_id == u.id,
59
      left_join: m in subquery(messages),
60
      on: m.visitor_id == v.id,
61
      left_join: i in Issue,
62
      on: i.author_user_id == u.id,
63
      group_by: [u.id, m.count, m.last_at],
64
      order_by: [desc: u.inserted_at],
65
      limit: ^limit,
66
      offset: ^offset,
67
      select: %{
68
        id: u.id,
69
        github_login: u.github_login,
70
        github_name: u.github_name,
71
        github_avatar_url: u.github_avatar_url,
72
        status: u.status,
73
        joined_at: u.inserted_at,
74
        last_authenticated_at: u.last_authenticated_at,
75
        message_count: coalesce(m.count, 0),
76
        last_message_at: m.last_at,
77
        issue_count: count(i.id, :distinct)
78
      }
79
    )
80
    |> Repo.all()
81
  end
82
83
  @doc "How many accounts exist."
84
  @spec count_accounts() :: non_neg_integer()
85
  def count_accounts, do: Repo.aggregate(User, :count, :id)
86
30 87
  @doc """
31 88
  Voice calls newest first, across every account.
32 89
lib/openagents_web/components/layouts.ex modified +3 -6

@@ -211,6 +211,9 @@ defmodule OpenAgentsWeb.Layouts do

211 211
212 212
    ~H"""
213 213
    <footer class="sidebar-footer">
214
      <.link navigate={~p"/leaderboard"} class="sidebar-footer__link">
215
        <UI.icon name="trophy-top" /> Leaderboard
216
      </.link>
214 217
      <.link
215 218
        :if={@admin_link?}
216 219
        id="open-admin"

@@ -564,12 +567,6 @@ defmodule OpenAgentsWeb.Layouts do

564 567
          icon="brain"
565 568
          patchable={false}
566 569
        />
567
        <Layouts.sidebar_link
568
          path={~p"/leaderboard"}
569
          label="Leaderboard"
570
          icon="trophy-top"
571
          patchable={false}
572
        />
573 570
      </nav>
574 571
575 572
      <%!-- Rows the current page contributes. Chat's destinations, work
test/openagents_web/live/chat_live_test.exs modified +4 -2

@@ -52,7 +52,7 @@ defmodule OpenAgentsWeb.ChatLiveTest do

52 52
           end)
53 53
  end
54 54
55
  test "the sidebar carries Computers, Memory and Leaderboard for everyone", %{conn: conn} do
55
  test "the sidebar carries the shared destinations for everyone", %{conn: conn} do
56 56
    conn = log_in_github_user(conn, "computers-nav-browser")
57 57
    {:ok, view, _html} = live(conn, ~p"/chat")
58 58

@@ -60,7 +60,9 @@ defmodule OpenAgentsWeb.ChatLiveTest do

60 60
    # sit in the application sidebar rather than appearing only on chat.
61 61
    assert has_element?(view, ~s(#sidebar a.sidebar-row__hit[href="/computers"]))
62 62
    assert has_element?(view, ~s(#sidebar a.sidebar-row__hit[href="/chat?panel=memory"]))
63
    assert has_element?(view, ~s(#sidebar a.sidebar-row__hit[href="/leaderboard"]))
63
    # Leaderboard is a secondary destination, so it sits in the footer with the
64
    # docs and the component library rather than in the working nav.
65
    assert has_element?(view, ~s(#sidebar .sidebar-footer a[href="/leaderboard"]))
64 66
  end
65 67
66 68
  test "chat contributes its rows to the one application sidebar", %{conn: conn} do

This page updates live while a promote is in flight · changelog