admin live

007f0a2 · gmorell · 2026-06-10 23:30

10 files +117 -3

Files changed

modified README.md
+2 −1
@@ -158,7 +158,8 @@ the account menu, linking to `/admin`. From there an oper can create
158 158 invites (note + optional expiry), copy each single-use signup link,
159 159 revoke outstanding invites, see every registered channel network-wide
160 160 (the operator counterpart to the per-user "Registered channels you can
161 −manage" page), and see everyone currently connected to the network —
161 +manage" page) plus any unregistered channels currently live in the
162 +cluster, and see everyone currently connected to the network —
162 163 each tagged registered / anonymous / service, with their account, host,
163 164 and node. The page is gated to opers — see
164 165 [Creating an IRC operator](#creating-an-irc-operator) for the flag.
modified lib/oh_my_ex_oh_ircd_web/live/admin_live.ex
+66 −2
@@ -78,8 +78,23 @@ defmodule OhMyExOhIrcdWeb.AdminLive do
78 78 defp load_invites(socket), do: assign(socket, :invites, Accounts.list_active_invites())
79 79
80 80 defp load_channels(socket) do
81 − entries = Channels.list_with_founder() |> Enum.map(&enrich/1)
82 − assign(socket, :channels, entries)
81 + registered = Channels.list_with_founder() |> Enum.map(&enrich/1)
82 + registered_names = MapSet.new(registered, &String.downcase(&1.name))
83 +
84 + # Channels live in the cluster (a running singleton) but with no DB
85 + # registration — e.g. someone JOINed a fresh channel that was never
86 + # `REGISTER`ed. Pull them from the Horde registry and drop any that
87 + # are already covered by the registered list.
88 + unregistered =
89 + Stats.channel_names()
90 + |> Enum.reject(&MapSet.member?(registered_names, String.downcase(&1)))
91 + |> Enum.map(&enrich_live/1)
92 + |> Enum.reject(&is_nil/1)
93 + |> Enum.sort_by(&String.downcase(&1.name))
94 +
95 + socket
96 + |> assign(:channels, registered)
97 + |> assign(:unregistered_channels, unregistered)
83 98 end
84 99
85 100 defp load_users(socket) do
@@ -109,6 +124,21 @@ defmodule OhMyExOhIrcdWeb.AdminLive do
109 124 }
110 125 end
111 126
127 + # Live snapshot for a channel that has no DB record — everything comes
128 + # from the running singleton.
129 + defp enrich_live(name) do
130 + snap = Channel.mode_snapshot(name)
131 +
132 + %{
133 + name: name,
134 + topic: Channel.topic(name) || "",
135 + member_count: length(Channel.members(name)),
136 + mode_string: render_mode_string(snap.flags, snap.key, snap.limit)
137 + }
138 + catch
139 + _, _ -> nil
140 + end
141 +
112 142 defp render_mode_string(flags, key, limit) do
113 143 flag_letters =
114 144 ["i", "m", "n", "f"]
@@ -225,6 +255,40 @@ defmodule OhMyExOhIrcdWeb.AdminLive do
225 255 <% end %>
226 256 </section>
227 257
258 + <section class="space-y-4" id="unregistered-channels">
259 + <h2 class="text-lg font-mono font-semibold">Unregistered channels (live)</h2>
260 + <p class="text-xs text-base-content/60">
261 + Channels in use right now that have no ChanServ registration.
262 + </p>
263 +
264 + <%= if @unregistered_channels == [] do %>
265 + <div class="rounded-box border border-base-300 bg-base-200 p-6 text-sm text-base-content/70">
266 + No unregistered channels are live right now.
267 + </div>
268 + <% else %>
269 + <div class="overflow-x-auto rounded-box border border-base-300">
270 + <table class="table table-zebra">
271 + <thead>
272 + <tr>
273 + <th>Channel</th>
274 + <th>Members</th>
275 + <th>Modes</th>
276 + <th>Topic</th>
277 + </tr>
278 + </thead>
279 + <tbody>
280 + <tr :for={c <- @unregistered_channels}>
281 + <td class="font-mono">{c.name}</td>
282 + <td class="tabular-nums">{c.member_count}</td>
283 + <td class="font-mono text-xs">{c.mode_string}</td>
284 + <td class="max-w-md truncate">{c.topic}</td>
285 + </tr>
286 + </tbody>
287 + </table>
288 + </div>
289 + <% end %>
290 + </section>
291 +
228 292 <section class="space-y-4">
229 293 <div class="flex items-baseline gap-3 flex-wrap">
230 294 <h2 class="text-lg font-mono font-semibold">Users on the network</h2>
added test/oh_my_ex_oh_ircd_web/live/admin_live_channels_test.exs
+48 −0
@@ -0,0 +1,48 @@
1 +defmodule OhMyExOhIrcdWeb.AdminLiveChannelsTest do
2 + @moduledoc """
3 + The admin channel views read live `Channel` singletons, which boot
4 + their own processes and hit the DB in `init/1`. That needs the shared
5 + sandbox, so this runs `async: false` (unlike the main AdminLive test).
6 + """
7 +
8 + use OhMyExOhIrcdWeb.ConnCase, async: false
9 +
10 + import Phoenix.LiveViewTest
11 + import OhMyExOhIrcd.AccountsFixtures
12 +
13 + alias OhMyExOhIrcd.{Accounts, Channels, IRC.Channel}
14 +
15 + defp oper_conn(conn) do
16 + user = user_fixture()
17 + {:ok, user} = Accounts.set_oper(user, true)
18 + log_in_user(conn, user)
19 + end
20 +
21 + test "a live, unregistered channel shows under 'Unregistered channels'", %{conn: conn} do
22 + name = "#live#{System.unique_integer([:positive])}"
23 + {:ok, _pid} = Channel.ensure_started(name)
24 +
25 + {:ok, _lv, html} = conn |> oper_conn() |> live(~p"/admin")
26 +
27 + assert html =~ "Unregistered channels"
28 + assert html =~ name
29 + end
30 +
31 + test "a registered channel does NOT appear in the unregistered section", %{conn: conn} do
32 + founder = user_fixture()
33 + name = "#reg#{System.unique_integer([:positive])}"
34 + {:ok, _rec} = Channels.register(name, founder)
35 + # Start its singleton too, so it's live *and* registered.
36 + {:ok, _pid} = Channel.ensure_started(name)
37 +
38 + {:ok, lv, _html} = conn |> oper_conn() |> live(~p"/admin")
39 +
40 + # It appears on the page (registered table)...
41 + assert render(lv) =~ name
42 +
43 + # ...but is excluded from the unregistered section, since enrich_live
44 + # skips anything already covered by list_with_founder.
45 + unregistered = lv |> element("#unregistered-channels") |> render()
46 + refute unregistered =~ name
47 + end
48 +end
modified test/oh_my_ex_oh_ircd_web/live/admin_live_test.exs
+1 −0
@@ -27,6 +27,7 @@ defmodule OhMyExOhIrcdWeb.AdminLiveTest do
27 27 assert html =~ "Admin console"
28 28 assert html =~ "Registration invites"
29 29 assert html =~ "All registered channels"
30 + assert html =~ "Unregistered channels (live)"
30 31 assert html =~ "Users on the network"
31 32 end
32 33 end

Parents: 0aa3d5d