One shell everywhere: no nested command bars, owner is always an operator

1b9c135724c4 · AtlantisPleb · · parent d491c17f7214

One shell everywhere: no nested command bars, owner is always an operator

Nine pages built a second `Layouts.command_bar` inside the one the layout
already renders, each with its own brand mark and a chip back to the
conversation. That is the shape chat had: an application shell nested in an
application shell. All nine are gone; the page's name goes to `Layouts.app`
as a title, and Chat is a sidebar row, so the chip has nothing left to do.
Four more surfaces -- computers, leaderboard, code blob, the gallery -- never
passed `current_scope` and so rendered with no sidebar at all. They pass it
via `assigns[:current_scope]`, since not every live_session assigns the key
and the layout already reads nil as "signed out, no sidebar".

The owner is now an operator by construction rather than by configuration.
`runtime.exs` replaces the admin list wholesale from an environment variable,
so an unset or mistyped value could lock the owner out of the surface used to
fix it; the id is unioned in at the source instead, where no environment can
drop it. Banning still revokes it.

Login is one component. It was a link to the homepage's anchor in the command
bar and a hand-rolled form on the homepage; a control labelled "log in" that
navigates somewhere else is lying about what it does. Both are now
`UI.github_login/1`, which posts for real and goes pending on submit: the
OAuth round-trip leaves the page, so without it there is a window where the
button looks idle and clickable while a redirect is in flight, and a second
click makes a second OAuth attempt. Pending is expressed for `:disabled`
alone as well as for the hook's attribute, so a browser restoring the page on
back-navigation still shows the right thing.

Two layout fixes found on the way. The composer autofocuses on mount, and
Basecoat's `.textarea:focus` paints a 3px ring, so a second rounded rectangle
was drawn inside the composer card from the moment the page loaded -- the card
already states focus through `:focus-within`. And the memory header's prose
column had no flex basis, so four text buttons squeezed the heading and its
paragraph into a ten-character ribbon.

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 assets/css/app.css
  • modified assets/css/openagents.css
  • modified lib/openagents/accounts.ex
  • modified lib/openagents_web/component_catalog.ex
  • modified lib/openagents_web/components/layouts.ex
  • modified lib/openagents_web/components/ui.ex
  • modified lib/openagents_web/live/admin_live.ex
  • modified lib/openagents_web/live/admin_scv_accounts_live.ex
  • modified lib/openagents_web/live/changelog_live.ex
  • modified lib/openagents_web/live/code_blob_live.ex
  • modified lib/openagents_web/live/code_commit_live.ex
  • modified lib/openagents_web/live/code_repo_live.ex
  • modified lib/openagents_web/live/components_live.ex
  • modified lib/openagents_web/live/computers_live.ex
  • modified lib/openagents_web/live/home_live.ex
  • modified lib/openagents_web/live/leaderboard_live.ex
  • modified lib/openagents_web/live/network_status_live.ex
  • modified lib/openagents_web/live/ui_gallery_live.ex
  • modified test/openagents/accounts_test.exs
  • modified test/openagents_web/live/admin_live_test.exs
  • modified test/openagents_web/live/changelog_live_test.exs
  • modified test/openagents_web/live/computers_navigation_test.exs
  • modified test/openagents_web/live/leaderboard_live_test.exs

Diff

23 files changed, +272 -167

assets/css/app.css modified +12

@@ -584,6 +584,18 @@

584 584
  color: var(--text-muted);
585 585
}
586 586
587
/* The card is the control, so the card carries the focus state. Basecoat's
588
   `.textarea:focus` paints a 3px ring, and the composer autofocuses on mount,
589
   so that ring drew a second rounded rectangle inside the card from the moment
590
   the page loaded. `.composer-card:focus-within` already states the same
591
   thing, once, on the right element. */
592
.composer-input:focus,
593
.composer-input:focus-visible {
594
  border: 0;
595
  box-shadow: none;
596
  outline: none;
597
}
598
587 599
.composer-trailing {
588 600
  display: flex;
589 601
  align-items: center;
assets/css/openagents.css modified +79 -2

@@ -2947,15 +2947,41 @@

2947 2947
    margin-inline: auto;
2948 2948
  }
2949 2949
2950
  /* The prose column needs a basis. Without one it shrinks to fit whatever the
2951
     action row demands, and that row holds four text buttons -- so the heading
2952
     and its paragraph were squeezed into a ten-character ribbon while the
2953
     controls took the width. */
2950 2954
  .memory-header {
2951 2955
    display: flex;
2952
    align-items: center;
2956
    flex-wrap: wrap;
2957
    align-items: flex-start;
2953 2958
    justify-content: space-between;
2954
    gap: 12px;
2959
    gap: 12px 16px;
2960
  }
2961
2962
  .memory-header > :first-child {
2963
    flex: 1 1 22rem;
2964
    min-width: 0;
2965
  }
2966
2967
  .memory-header h1 {
2968
    color: var(--text-primary);
2969
    font-size: 1.125rem;
2970
    font-weight: 600;
2971
    line-height: 1.4;
2972
  }
2973
2974
  .memory-header p {
2975
    max-width: 60ch;
2976
    padding-block-start: 4px;
2977
    color: var(--text-muted);
2978
    font-size: 0.875rem;
2979
    line-height: 1.5;
2955 2980
  }
2956 2981
2957 2982
  .memory-header__actions {
2958 2983
    display: flex;
2984
    flex-wrap: wrap;
2959 2985
    align-items: center;
2960 2986
    gap: 8px;
2961 2987
  }

@@ -4104,3 +4130,54 @@

4104 4130
    font-size: 0.8125rem;
4105 4131
  }
4106 4132
}
4133
4134
/* ── Login ────────────────────────────────────────────────────────────────── */
4135
4136
/* Signing in leaves the page, so between the click and the redirect there is a
4137
 * window where an idle-looking button invites a second click and a second
4138
 * OAuth attempt. The pending state closes it.
4139
 *
4140
 * Both `[data-pending]` and `:disabled` select it: the hook sets the first, but
4141
 * a browser restoring the page on back-navigation, or one running no script at
4142
 * all, should still not show a spinner-less disabled button. */
4143
4144
@layer components {
4145
  .login-form {
4146
    margin: 0;
4147
  }
4148
4149
  .login-button__spinner {
4150
    display: none;
4151
    animation: login-spin 900ms linear infinite;
4152
  }
4153
4154
  .login-button[data-pending] .login-button__mark,
4155
  .login-button:disabled .login-button__mark {
4156
    display: none;
4157
  }
4158
4159
  .login-button[data-pending] .login-button__spinner,
4160
  .login-button:disabled .login-button__spinner {
4161
    display: inline-flex;
4162
  }
4163
4164
  .login-button:disabled {
4165
    cursor: not-allowed;
4166
    opacity: 0.6;
4167
  }
4168
4169
  @keyframes login-spin {
4170
    to {
4171
      rotate: 360deg;
4172
    }
4173
  }
4174
4175
  /* The spinner is the only thing reporting that the round-trip started, so it
4176
     keeps turning; the disabled state and the swapped glyph already carry the
4177
     meaning without it, which is what a reduced-motion reader gets. */
4178
  @media (prefers-reduced-motion: reduce) {
4179
    .login-button__spinner {
4180
      animation: none;
4181
    }
4182
  }
4183
}
lib/openagents/accounts.ex modified +17 -4

@@ -163,12 +163,25 @@ defmodule OpenAgents.Accounts do

163 163
164 164
  def admin?(_user), do: false
165 165
166
  @doc "The configured operator GitHub IDs."
166
  # The owner, by GitHub's immutable numeric id. Operator access is otherwise
167
  # configured per environment, and `runtime.exs` replaces the list wholesale
168
  # from `OPENAGENTS_ADMIN_GITHUB_IDS` -- so an environment whose variable is
169
  # unset, mistyped, or lost in a redeploy would lock the owner out of the very
170
  # surface used to fix it. Unioned in here rather than defaulted in config so
171
  # no environment can drop it.
172
  @owner_github_id 14_167_547
173
174
  @doc """
175
  The operator GitHub IDs: the owner, plus whatever this environment configures.
176
  """
167 177
  @spec admin_github_ids() :: [pos_integer()]
168 178
  def admin_github_ids do
169
    :openagents
170
    |> Application.get_env(:admin_github_ids, [])
171
    |> Enum.filter(&(is_integer(&1) and &1 > 0))
179
    configured =
180
      :openagents
181
      |> Application.get_env(:admin_github_ids, [])
182
      |> Enum.filter(&(is_integer(&1) and &1 > 0))
183
184
    Enum.uniq([@owner_github_id | configured])
172 185
  end
173 186
174 187
  @doc false
lib/openagents_web/component_catalog.ex modified +7

@@ -65,6 +65,13 @@ defmodule OpenAgentsWeb.ComponentCatalog do

65 65
          source: "OpenAgentsWeb.UI.breadcrumb/1",
66 66
          summary: "Ancestor trail ending in the current page, which is not a link."
67 67
        },
68
        %{
69
          slug: "openagents-github-login",
70
          title: "GitHub login",
71
          icon: "brand-github",
72
          source: "OpenAgentsWeb.UI.github_login/1",
73
          summary: "Sign-in form that goes pending on submit."
74
        },
68 75
        %{
69 76
          slug: "openagents-copy-button",
70 77
          title: "Copy button",
lib/openagents_web/components/layouts.ex modified +4 -3

@@ -131,9 +131,10 @@ defmodule OpenAgentsWeb.Layouts do

131 131
        <%= if @current_scope do %>
132 132
          <.account_dropdown current_scope={@current_scope} />
133 133
        <% else %>
134
          <.button navigate={~p"/#github-tools"} variant={:primary} size={:sm}>
135
            <UI.icon name="brand-github" /> Log in with GitHub
136
          </.button>
134
          <%!-- A real sign-in, not a link to the homepage's anchor: a control
135
          labelled "log in" that navigates somewhere else instead is lying
136
          about what it does. --%>
137
          <UI.github_login id="command-bar-signin" size={:sm} />
137 138
        <% end %>
138 139
      </div>
139 140
    </header>
lib/openagents_web/components/ui.ex modified +69

@@ -820,6 +820,75 @@ defmodule OpenAgentsWeb.UI do

820 820
    """
821 821
  end
822 822
823
  @doc """
824
  The GitHub sign-in control.
825
826
  A real form POST, not a link: signing in starts an OAuth round-trip, and a
827
  control labelled "log in" that navigates somewhere else instead is lying
828
  about what it does.
829
830
  The round-trip leaves the page, so there is a window where the button looks
831
  idle and clickable while a redirect is already in flight. Submitting swaps
832
  the mark for a spinner and disables the control, which both reports that
833
  something is happening and stops a second submission creating a second OAuth
834
  attempt.
835
836
  The pending state is applied by a hook, but it is also expressed for
837
  `:disabled` alone, so a browser that re-enables the button on back-navigation
838
  or runs no script still shows the right thing.
839
  """
840
  attr :id, :string, required: true
841
  attr :label, :string, default: "Log in with GitHub"
842
  attr :variant, :atom, default: :primary
843
  attr :size, :atom, default: :md
844
845
  attr :action, :string,
846
    default: "/auth/github?github_tools=enabled",
847
    doc: "where the sign-in posts; the caller owns the route"
848
849
  attr :class, :any, default: nil
850
  attr :rest, :global
851
852
  def github_login(assigns) do
853
    ~H"""
854
    <.form
855
      for={%{}}
856
      as={:auth}
857
      id={"#{@id}-form"}
858
      action={@action}
859
      method="post"
860
      class="login-form"
861
      phx-hook=".LoginPending"
862
      {@rest}
863
    >
864
      <.button id={@id} type="submit" variant={@variant} size={@size} class={["login-button", @class]}>
865
        <.icon name="brand-github" class="login-button__mark" />
866
        <.icon name="circle-dashed" class="login-button__spinner" />
867
        {@label}
868
      </.button>
869
    </.form>
870
    <script :type={Phoenix.LiveView.ColocatedHook} name=".LoginPending">
871
      export default {
872
        mounted() {
873
          this.button = this.el.querySelector("button[type=submit]")
874
          this.onSubmit = () => {
875
            if (!this.button) return
876
            this.button.dataset.pending = "true"
877
            // Disabled after the event, not during it: disabling a submit
878
            // button inside its own submit handler cancels the submission in
879
            // some browsers.
880
            window.setTimeout(() => { this.button.disabled = true }, 0)
881
          }
882
          this.el.addEventListener("submit", this.onSubmit)
883
        },
884
        destroyed() {
885
          this.el.removeEventListener("submit", this.onSubmit)
886
        },
887
      }
888
    </script>
889
    """
890
  end
891
823 892
  @doc """
824 893
  A control that copies text to the clipboard and reports that it did.
825 894
lib/openagents_web/live/admin_live.ex modified +1 -13

@@ -68,24 +68,12 @@ defmodule OpenAgentsWeb.AdminLive do

68 68
  @impl true
69 69
  def render(assigns) do
70 70
    ~H"""
71
    <Layouts.app flash={@flash} current_scope={@current_scope}>
71
    <Layouts.app flash={@flash} current_scope={@current_scope} title="Admin">
72 72
      <main id="admin-page" class="app-shell admin-shell">
73 73
        <%!-- The same bar every other surface renders, so moving between them
74 74
              reads as one application. The lockup carries only the way back:
75 75
              nothing in the product links here, and this is not a place to
76 76
              navigate onward from. --%>
77
        <Layouts.command_bar aria_label="OpenAgents operator panel" current_user={@current_user}>
78
          <:lockup>
79
            <.button
80
              id="return-to-conversation"
81
              variant={:chip}
82
              size={:xs}
83
              phx-click={JS.navigate(~p"/chat")}
84
            >
85
              <.icon name="arrow-left" /> RETURN TO CONVERSATION
86
            </.button>
87
          </:lockup>
88
        </Layouts.command_bar>
89 77
90 78
        <section class="admin" aria-label="Voice call recordings">
91 79
          <header class="admin-heading">
lib/openagents_web/live/admin_scv_accounts_live.ex modified +1 -14

@@ -104,21 +104,8 @@ defmodule OpenAgentsWeb.AdminScvAccountsLive do

104 104
  @impl true
105 105
  def render(assigns) do
106 106
    ~H"""
107
    <Layouts.app flash={@flash} current_scope={@current_scope}>
107
    <Layouts.app flash={@flash} current_scope={@current_scope} title="SCV Codex accounts">
108 108
      <main id="admin-scv-accounts-page" class="app-shell admin-shell">
109
        <Layouts.command_bar aria_label="SCV Codex account settings" current_user={@current_user}>
110
          <:lockup>
111
            <.button
112
              id="return-to-operator"
113
              variant={:chip}
114
              size={:xs}
115
              phx-click={JS.navigate(~p"/admin")}
116
            >
117
              <.icon name="arrow-left" /> OPERATOR
118
            </.button>
119
          </:lockup>
120
        </Layouts.command_bar>
121
122 109
        <section class="admin space-y-8" aria-labelledby="scv-codex-heading">
123 110
          <header class="admin-heading">
124 111
            <h1 id="scv-codex-heading">Codex accounts for SCVs</h1>
lib/openagents_web/live/changelog_live.ex modified +1 -15

@@ -90,22 +90,8 @@ defmodule OpenAgentsWeb.ChangelogLive do

90 90
  @impl true
91 91
  def render(assigns) do
92 92
    ~H"""
93
    <Layouts.app flash={@flash} current_scope={@current_scope}>
93
    <Layouts.app flash={@flash} current_scope={@current_scope} title="Changelog">
94 94
      <main id="changelog-page" class="app-shell changelog-shell">
95
        <Layouts.command_bar aria_label="OpenAgents changelog" current_user={@current_user}>
96
          <:lockup>
97
            <.button
98
              :if={@current_user}
99
              id="return-to-conversation"
100
              variant={:chip}
101
              size={:xs}
102
              phx-click={JS.navigate(~p"/chat")}
103
            >
104
              <.icon name="arrow-left" /> RETURN TO CONVERSATION
105
            </.button>
106
          </:lockup>
107
        </Layouts.command_bar>
108
109 95
        <section class="changelog" aria-label="Changelog">
110 96
          <header class="changelog-heading">
111 97
            <div>
lib/openagents_web/live/code_blob_live.ex modified +1 -14

@@ -75,21 +75,8 @@ defmodule OpenAgentsWeb.CodeBlobLive do

75 75
  @impl true
76 76
  def render(assigns) do
77 77
    ~H"""
78
    <Layouts.app flash={@flash}>
78
    <Layouts.app flash={@flash} current_scope={assigns[:current_scope]} title="Code">
79 79
      <main id="code-blob-page" class="app-shell code-shell">
80
        <Layouts.command_bar aria_label="Sarah code" current_user={@current_user}>
81
          <:lockup>
82
            <.button
83
              id="code-back-to-changelog"
84
              variant={:chip}
85
              size={:xs}
86
              phx-click={JS.navigate("/changelog")}
87
            >
88
              <.icon name="arrow-left" /> CHANGELOG
89
            </.button>
90
          </:lockup>
91
        </Layouts.command_bar>
92
93 80
        <section class="code" aria-label="File view">
94 81
          <header class="code-heading">
95 82
            <div>
lib/openagents_web/live/code_commit_live.ex modified +1 -14

@@ -123,21 +123,8 @@ defmodule OpenAgentsWeb.CodeCommitLive do

123 123
  @impl true
124 124
  def render(assigns) do
125 125
    ~H"""
126
    <Layouts.app flash={@flash} current_scope={@current_scope}>
126
    <Layouts.app flash={@flash} current_scope={@current_scope} title="Commit">
127 127
      <main id="code-commit-page" class="app-shell code-shell">
128
        <Layouts.command_bar aria_label="Sarah code" current_user={@current_user}>
129
          <:lockup>
130
            <.button
131
              id="code-back-to-repo"
132
              variant={:chip}
133
              size={:xs}
134
              phx-click={JS.navigate(@base)}
135
            >
136
              <.icon name="arrow-left" /> {@repo}
137
            </.button>
138
          </:lockup>
139
        </Layouts.command_bar>
140
141 128
        <section class="code" aria-label="Commit view">
142 129
          <header class="code-heading">
143 130
            <div>
lib/openagents_web/live/code_repo_live.ex modified +1 -15

@@ -55,22 +55,8 @@ defmodule OpenAgentsWeb.CodeRepoLive do

55 55
  @impl true
56 56
  def render(assigns) do
57 57
    ~H"""
58
    <Layouts.app flash={@flash} current_scope={@current_scope}>
58
    <Layouts.app flash={@flash} current_scope={@current_scope} title="Code">
59 59
      <main id="code-repo-page" class="app-shell code-shell">
60
        <Layouts.command_bar aria_label="OpenAgents code" current_user={@current_user}>
61
          <:lockup>
62
            <.button
63
              :if={@current_user}
64
              id="return-to-conversation"
65
              variant={:chip}
66
              size={:xs}
67
              phx-click={JS.navigate(~p"/chat")}
68
            >
69
              <.icon name="arrow-left" /> RETURN TO CONVERSATION
70
            </.button>
71
          </:lockup>
72
        </Layouts.command_bar>
73
74 60
        <section class="code" aria-label="Repository">
75 61
          <header class="code-heading">
76 62
            <div>
lib/openagents_web/live/components_live.ex modified +26

@@ -958,6 +958,32 @@ defmodule OpenAgentsWeb.ComponentsLive do

958 958
    """
959 959
  end
960 960
961
  defp component_demo(%{item: %{slug: "openagents-github-login"}} = assigns) do
962
    ~H"""
963
    <div class="space-y-3">
964
      <p class="text-sm text-base-content/60">
965
        A real form POST, because signing in starts an OAuth round-trip. Submitting
966
        swaps the mark for a spinner and disables the control: the round-trip leaves
967
        the page, so without it there is a window where the button looks idle and
968
        clickable while a redirect is already in flight, and a second click makes a
969
        second OAuth attempt. The pending state is also expressed for <code>:disabled</code>
970
        alone, so a browser restoring the page on
971
        back-navigation still shows the right thing.
972
      </p>
973
      <div class="flex flex-wrap items-center gap-3">
974
        <UI.github_login id="demo-github-login" />
975
        <UI.button variant={:primary} class="login-button" disabled>
976
          <UI.icon name="brand-github" class="login-button__mark" />
977
          <UI.icon name="circle-dashed" class="login-button__spinner" /> Log in with GitHub
978
        </UI.button>
979
      </div>
980
      <p class="text-sm text-base-content/60">
981
        The second is the pending state, shown by disabling it directly.
982
      </p>
983
    </div>
984
    """
985
  end
986
961 987
  # ── Landing ───────────────────────────────────────────────────────────────
962 988
  #
963 989
  # These demo at reduced scale inside the documentation column. A hero is
lib/openagents_web/live/computers_live.ex modified +1 -14

@@ -192,21 +192,8 @@ defmodule OpenAgentsWeb.ComputersLive do

192 192
  @impl true
193 193
  def render(assigns) do
194 194
    ~H"""
195
    <Layouts.app flash={@flash}>
195
    <Layouts.app flash={@flash} current_scope={assigns[:current_scope]} title="Computers">
196 196
      <main id="computers-page" class="app-shell computers-shell">
197
        <Layouts.command_bar aria_label="Sarah computers" current_user={@current_user}>
198
          <:lockup>
199
            <.button
200
              id="return-to-conversation"
201
              variant={:chip}
202
              size={:xs}
203
              phx-click={JS.navigate(~p"/chat")}
204
            >
205
              <.icon name="arrow-left" /> RETURN TO CONVERSATION
206
            </.button>
207
          </:lockup>
208
        </Layouts.command_bar>
209
210 197
        <section id="computers-manager" class="computers" aria-label="Paired computers">
211 198
          <div class="computers__inner">
212 199
            <header class="computers-heading">
lib/openagents_web/live/home_live.ex modified +1 -11

@@ -48,17 +48,7 @@ defmodule OpenAgentsWeb.HomeLive do

48 48
                View issues
49 49
              </.button>
50 50
            <% else %>
51
              <.form
52
                for={%{}}
53
                as={:auth}
54
                action={~p"/auth/github?github_tools=enabled"}
55
                method="post"
56
                class="m-0"
57
              >
58
                <.button type="submit" variant={:primary} size={:lg} id="home-cta-signin">
59
                  <.icon name="brand-github" /> Log in with GitHub
60
                </.button>
61
              </.form>
51
              <.github_login id="home-cta-signin" size={:lg} />
62 52
              <%!-- Quieter than the action beside it. `variant` defaults to
63 53
              `:primary`, so two filled buttons sat side by side stating that
64 54
              both were the thing to do, which leaves a reader picking rather
lib/openagents_web/live/leaderboard_live.ex modified +1 -15

@@ -37,22 +37,8 @@ defmodule OpenAgentsWeb.LeaderboardLive do

37 37
  @impl true
38 38
  def render(assigns) do
39 39
    ~H"""
40
    <Layouts.app flash={@flash}>
40
    <Layouts.app flash={@flash} current_scope={assigns[:current_scope]} title="Leaderboard">
41 41
      <main id="leaderboard-page" class="app-shell leaderboard-shell">
42
        <Layouts.command_bar aria_label="OpenAgents leaderboard" current_user={@current_user}>
43
          <:lockup>
44
            <.button
45
              :if={@current_user}
46
              id="return-to-conversation"
47
              variant={:chip}
48
              size={:xs}
49
              phx-click={JS.navigate(~p"/chat")}
50
            >
51
              <.icon name="arrow-left" /> RETURN TO CONVERSATION
52
            </.button>
53
          </:lockup>
54
        </Layouts.command_bar>
55
56 42
        <section class="leaderboard" aria-label="Token leaderboard">
57 43
          <header class="leaderboard-heading">
58 44
            <h1>Leaderboard</h1>
lib/openagents_web/live/network_status_live.ex modified +1 -15

@@ -274,22 +274,8 @@ defmodule OpenAgentsWeb.NetworkStatusLive do

274 274
  @impl true
275 275
  def render(assigns) do
276 276
    ~H"""
277
    <Layouts.app flash={@flash} current_scope={@current_scope}>
277
    <Layouts.app flash={@flash} current_scope={@current_scope} title="Status">
278 278
      <main id="network-status-page" class="app-shell status-shell">
279
        <Layouts.command_bar aria_label="OpenAgents network status" current_user={@current_user}>
280
          <:lockup>
281
            <.button
282
              :if={@current_user}
283
              id="return-to-conversation"
284
              variant={:chip}
285
              size={:xs}
286
              phx-click={JS.navigate(~p"/chat")}
287
            >
288
              <.icon name="arrow-left" /> RETURN TO CONVERSATION
289
            </.button>
290
          </:lockup>
291
        </Layouts.command_bar>
292
293 279
        <section class="status" aria-label="Network status">
294 280
          <header class="status-heading">
295 281
            <div>
test/openagents/accounts_test.exs modified +26

@@ -121,4 +121,30 @@ defmodule OpenAgents.AccountsTest do

121 121
      github_avatar_url: avatar_url || "https://avatars.githubusercontent.com/u/#{id}?v=4"
122 122
    }
123 123
  end
124
125
  describe "operator identity" do
126
    test "the owner is an operator no matter what the environment configures" do
127
      original = Application.get_env(:openagents, :admin_github_ids)
128
      on_exit(fn -> Application.put_env(:openagents, :admin_github_ids, original) end)
129
130
      # `runtime.exs` replaces this list wholesale from an environment
131
      # variable, so an unset or mistyped value must not be able to lock the
132
      # owner out of the surface used to fix it.
133
      Application.put_env(:openagents, :admin_github_ids, [])
134
      assert 14_167_547 in Accounts.admin_github_ids()
135
136
      Application.put_env(:openagents, :admin_github_ids, [999_999])
137
      ids = Accounts.admin_github_ids()
138
      assert 14_167_547 in ids
139
      assert 999_999 in ids
140
    end
141
142
    test "the owner is not an operator while banned" do
143
      {:ok, owner} = Accounts.upsert_github_user(profile(14_167_547, "AtlantisPleb"))
144
      assert Accounts.admin?(owner)
145
146
      {:ok, banned} = Accounts.ban_user(owner, "manual_abuse_review")
147
      refute Accounts.admin?(banned)
148
    end
149
  end
124 150
end
test/openagents_web/live/admin_live_test.exs modified +3 -3

@@ -194,9 +194,9 @@ defmodule OpenAgentsWeb.AdminLiveTest do

194 194
      conn = log_in_admin_user(conn, "admin-chrome-operator")
195 195
      {:ok, view, _html} = live(conn, ~p"/admin")
196 196
197
      assert has_element?(view, "header.command-bar")
198
      assert has_element?(view, "#return-to-conversation")
199
      assert has_element?(view, "#account-menu-trigger")
197
      refute has_element?(view, "header.command-bar")
198
      assert has_element?(view, ~s(#sidebar a.sidebar-row__hit[href="/chat"]))
199
      assert has_element?(view, "#account-bar-trigger")
200 200
      # Nothing navigates onward from here, and nothing in the product links in.
201 201
      refute has_element?(view, "#open-leaderboard")
202 202
    end
test/openagents_web/live/changelog_live_test.exs modified +7 -5

@@ -49,18 +49,20 @@ defmodule OpenAgentsWeb.ChangelogLiveTest do

49 49
  test "carries the shared command bar without account controls for a visitor", %{conn: conn} do
50 50
    {:ok, _view, html} = live(conn, ~p"/changelog")
51 51
52
    assert html =~ ~s(class="command-bar")
53
    refute html =~ ~s(id="account-menu-trigger")
52
    # The shell supplies the one command bar; the page no longer builds a second.
53
    refute html =~ ~s(class="command-bar")
54
    refute html =~ ~s(id="account-bar-trigger")
54 55
    refute html =~ ~s(id="return-to-conversation")
55 56
  end
56 57
57
  test "offers a way back to the conversation when logged in", %{conn: conn} do
58
  test "carries the shell sidebar when logged in", %{conn: conn} do
58 59
    conn = log_in_github_user(conn, "changelog-header-browser")
59 60
60 61
    {:ok, _view, html} = live(conn, ~p"/changelog")
61 62
62
    assert html =~ ~s(id="return-to-conversation")
63
    assert html =~ "RETURN TO CONVERSATION"
63
    # Chat is a sidebar row, so the page carries no chip back to it.
64
    refute html =~ ~s(id="return-to-conversation")
65
    assert html =~ ~s(href="/chat")
64 66
  end
65 67
66 68
  test "publishes no node internals", %{conn: conn} do
test/openagents_web/live/computers_navigation_test.exs modified +3 -3

@@ -8,9 +8,9 @@ defmodule OpenAgentsWeb.ComputersNavigationTest do

8 8
9 9
    assert has_element?(view, "#computers-page")
10 10
    assert has_element?(view, "#computers-manager")
11
    assert has_element?(view, "header.command-bar")
12
    assert has_element?(view, "#return-to-conversation")
13
    assert has_element?(view, "#account-menu-trigger")
11
    refute has_element?(view, "header.command-bar")
12
    assert has_element?(view, ~s(#sidebar a.sidebar-row__hit[href="/chat"]))
13
    assert has_element?(view, "#account-bar-trigger")
14 14
  end
15 15
16 16
  test "the legacy authenticated path redirects to the canonical URL without its query", %{
test/openagents_web/live/leaderboard_live_test.exs modified +8 -6

@@ -40,20 +40,22 @@ defmodule OpenAgentsWeb.LeaderboardLiveTest do

40 40
  test "carries the shared command bar without account controls for a visitor", %{conn: conn} do
41 41
    {:ok, _view, html} = live(conn, ~p"/leaderboard")
42 42
43
    assert html =~ ~s(class="command-bar")
43
    # The shell supplies the one command bar; the page no longer builds a second.
44
    refute html =~ ~s(class="command-bar")
44 45
    assert html =~ "OpenAgents"
45
    refute html =~ ~s(id="account-menu-trigger")
46
    refute html =~ ~s(id="account-bar-trigger")
46 47
    refute html =~ ~s(id="return-to-conversation")
47 48
  end
48 49
49
  test "offers the account menu and a way back to the conversation when logged in", %{conn: conn} do
50
  test "carries the shell account menu and sidebar when logged in", %{conn: conn} do
50 51
    conn = log_in_github_user(conn, "leaderboard-header-browser")
51 52
52 53
    {:ok, _view, html} = live(conn, ~p"/leaderboard")
53 54
54
    assert html =~ ~s(id="account-menu-trigger")
55
    assert html =~ ~s(id="return-to-conversation")
56
    assert html =~ "RETURN TO CONVERSATION"
55
    assert html =~ ~s(id="account-bar-trigger")
56
    # Chat is a sidebar row, so the page carries no chip back to it.
57
    refute html =~ ~s(id="return-to-conversation")
58
    assert html =~ ~s(href="/chat")
57 59
  end
58 60
59 61
  test "explains itself when no account has spent a token", %{conn: conn} do

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