neiam
/oh-my-ex-oh-ircd
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
admin live
007f0a2 · gmorell · 2026-06-10 23:30
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