From 3035bbb51fa5390de8bcb3a51eb956286cf73a34 Mon Sep 17 00:00:00 2001 From: penguinboi Date: Sat, 22 Aug 2026 23:08:35 -0400 Subject: [PATCH 1/7] WIP --- bot/extensions/dashboard_cog.py | 26 ++++++++++++++++++++++---- lib/dashboard/dashboard.py | 24 +++++++++++++++++++++++- pyproject.toml | 3 ++- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/bot/extensions/dashboard_cog.py b/bot/extensions/dashboard_cog.py index 4e6e314..9ae56ba 100644 --- a/bot/extensions/dashboard_cog.py +++ b/bot/extensions/dashboard_cog.py @@ -87,14 +87,32 @@ def _reset_daily_count(self) -> None: self._save_counts() def _report_member_count(self) -> None: - dachshund.emit( - "member_count", - value=sum(guild.member_count or 0 for guild in self.bot.guilds), - ) + # dachshund.emit( + # "member_count", + # value=sum(guild.member_count or 0 for guild in self.bot.guilds), + # ) + guild_id = 1349086642446532639 + guild = self.bot.get_guild(guild_id) + + if guild: + dachshund.emit( + "member_count", + value=guild.member_count, + ) + + def _load_users(self) -> None: + guild_id = 1349086642446532639 + guild = self.bot.get_guild(guild_id) + + # for guild in self.bot.guilds: + if guild: + for member in guild.members: + dachshund.emit("members", id=member.id, name=member.name, guild=guild.name, _guild_id=str(guild.id)) @Cog.listener() async def on_ready(self) -> None: self._report_member_count() + self._load_users() @Cog.listener() async def on_member_join(self, _member: Member) -> None: diff --git a/lib/dashboard/dashboard.py b/lib/dashboard/dashboard.py index fca0416..132ea30 100644 --- a/lib/dashboard/dashboard.py +++ b/lib/dashboard/dashboard.py @@ -6,7 +6,7 @@ Gauge, ) # registers the "gauge" widget type on import -DACHSHUND_EVENT_STORE = app.config.get("dachshund", "event_store") or "dachshund.db" +DACHSHUND_EVENT_STORE = app.config.get("dachshund", "event_store", "dachshund.db") def init_dachshund(): @@ -22,8 +22,29 @@ def start_dashboard(): host="0.0.0.0", ) +@dachshund.action("ban", label="Ban", confirm=True) +async def greet_user(name: str, guild_id: str) -> None: + from bot import bot + + if guild := bot.get_guild(int(guild_id)): + await guild.system_channel.send(f"Hello {name}!") def build_dashboard() -> None: + dachshund.widget( + "member_count", + type="number", + size=1, + ) + + dachshund.widget( + "members", + type="table", + columns=["id", "name", "guild", "_guild_id"], + key="id", + actions=["ban"], + size=2, + ) + # latency dachshund.chart( "latency", @@ -90,3 +111,4 @@ def build_dashboard() -> None: size=3, persist=True, ) + diff --git a/pyproject.toml b/pyproject.toml index 7745175..9527606 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,8 @@ dependencies = [ "googletrans==4.0.0-rc1", "openai==0.26.1", # TODO: point at a tagged release once dachshund stabilizes - "dachshund @ git+https://github.com/PenguinBoi12/dachshund.git@poc", +# "dachshund @ git+https://github.com/PenguinBoi12/dachshund.git@poc", + "dachshund @ file:///home/penguinboi/Projects/dachshund" ] [project.optional-dependencies] From 2ab383ca71c933ca6bff26ac146905befd19e85c Mon Sep 17 00:00:00 2001 From: penguinboi Date: Fri, 28 Aug 2026 01:40:14 -0400 Subject: [PATCH 2/7] WIP --- bot/extensions/dashboard_cog.py | 32 ++++--------- bot/grace.py | 2 +- lib/dashboard/dashboard.py | 39 +++++++++++++--- tests/extensions/test_dashboard_cog.py | 52 ++++++++++++++++----- tests/lib/dashboard/test_dashboard.py | 63 +++++++++++++++++++++++++- 5 files changed, 144 insertions(+), 44 deletions(-) diff --git a/bot/extensions/dashboard_cog.py b/bot/extensions/dashboard_cog.py index 9ae56ba..3a28369 100644 --- a/bot/extensions/dashboard_cog.py +++ b/bot/extensions/dashboard_cog.py @@ -8,6 +8,7 @@ from bot.grace import Grace from bot.models.extensions.widget_state import WidgetState +from lib.dashboard.dashboard import GUILD_IDS class DashboardCog( @@ -87,40 +88,27 @@ def _reset_daily_count(self) -> None: self._save_counts() def _report_member_count(self) -> None: - # dachshund.emit( - # "member_count", - # value=sum(guild.member_count or 0 for guild in self.bot.guilds), - # ) - guild_id = 1349086642446532639 - guild = self.bot.get_guild(guild_id) - - if guild: - dachshund.emit( - "member_count", - value=guild.member_count, - ) - - def _load_users(self) -> None: - guild_id = 1349086642446532639 - guild = self.bot.get_guild(guild_id) - - # for guild in self.bot.guilds: - if guild: - for member in guild.members: - dachshund.emit("members", id=member.id, name=member.name, guild=guild.name, _guild_id=str(guild.id)) + for guild_id in GUILD_IDS: + if guild := self.bot.get_guild(guild_id): + dachshund.emit( + "member_count", + value=guild.member_count, + ) @Cog.listener() async def on_ready(self) -> None: self._report_member_count() - self._load_users() + dachshund.emit("members") @Cog.listener() async def on_member_join(self, _member: Member) -> None: self._report_member_count() + dachshund.emit("members") @Cog.listener() async def on_member_remove(self, _member: Member) -> None: self._report_member_count() + dachshund.emit("members") @Cog.listener() async def on_message(self, _message: Message) -> None: diff --git a/bot/grace.py b/bot/grace.py index b8162da..5aca2f6 100644 --- a/bot/grace.py +++ b/bot/grace.py @@ -62,7 +62,7 @@ async def setup_hook(self): dashboard.init_dachshund() self.metrics = await DashboardMetrics.load() dashboard.build_dashboard() - dashboard.start_dashboard() + dashboard.start_dashboard({"bot": self}) async def on_ready(self): logger.info(f"{self.user.name}#{self.user.id} is online and ready to use!") diff --git a/lib/dashboard/dashboard.py b/lib/dashboard/dashboard.py index 132ea30..7031f67 100644 --- a/lib/dashboard/dashboard.py +++ b/lib/dashboard/dashboard.py @@ -7,6 +7,7 @@ ) # registers the "gauge" widget type on import DACHSHUND_EVENT_STORE = app.config.get("dachshund", "event_store", "dachshund.db") +GUILD_IDS: list[int] = [1349086642446532639] def init_dachshund(): @@ -15,20 +16,45 @@ def init_dachshund(): dachshund.default(path=DACHSHUND_EVENT_STORE, storage=SQLModelStorage()) -def start_dashboard(): +def start_dashboard(context: dict): dachshund.init( "Grace", description="Code Society's Discord Bot", host="0.0.0.0", + context=context ) -@dachshund.action("ban", label="Ban", confirm=True) -async def greet_user(name: str, guild_id: str) -> None: - from bot import bot +@dachshund.action("greet", label="Greet") +async def greet_user(ctx: dict, name: str, guild_id: str) -> None: + bot = ctx["bot"] if guild := bot.get_guild(int(guild_id)): await guild.system_channel.send(f"Hello {name}!") + +def load_members(ctx: dict, cursor: str | None, page_size: int): + bot = ctx["bot"] + members = [] + + for guild_id in GUILD_IDS: + if guild := bot.get_guild(guild_id): + for member in guild.members: + members.append( + { + "id": member.id, + "name": member.name, + "guild": guild.name, + "_guild_id": str(guild.id), + } + ) + + start = int(cursor) if cursor else 0 + rows = members[start : start + page_size] + next_cursor = str(start + page_size) if start + page_size < len(members) else None + + return rows, next_cursor + + def build_dashboard() -> None: dachshund.widget( "member_count", @@ -40,8 +66,8 @@ def build_dashboard() -> None: "members", type="table", columns=["id", "name", "guild", "_guild_id"], - key="id", - actions=["ban"], + actions=["greet"], + loader=load_members, size=2, ) @@ -111,4 +137,3 @@ def build_dashboard() -> None: size=3, persist=True, ) - diff --git a/tests/extensions/test_dashboard_cog.py b/tests/extensions/test_dashboard_cog.py index 4f12c6c..ad01284 100644 --- a/tests/extensions/test_dashboard_cog.py +++ b/tests/extensions/test_dashboard_cog.py @@ -40,14 +40,12 @@ def test_reset_daily_count__expect_persists_zeroed_count(dashboard_cog): @patch("bot.extensions.dashboard_cog.dachshund.emit") -def test_report_member_count__expect_emits_summed_member_count( +def test_report_member_count__expect_emits_member_count_for_configured_guild( mock_emit, dashboard_cog ): - """Verify _report_member_count sums member_count across every guild.""" - dashboard_cog.bot.guilds = [ - type("Guild", (), {"member_count": 10})(), - type("Guild", (), {"member_count": 5})(), - ] + """Verify _report_member_count emits the member count for each guild in GUILD_IDS.""" + guild = type("Guild", (), {"member_count": 15})() + dashboard_cog.bot.get_guild = lambda guild_id: guild dashboard_cog._report_member_count() @@ -58,33 +56,63 @@ def test_report_member_count__expect_emits_summed_member_count( @patch("bot.extensions.dashboard_cog.dachshund.emit") async def test_on_ready__expect_reports_member_count(mock_emit, dashboard_cog): """Verify on_ready reports the current member count.""" - dashboard_cog.bot.guilds = [type("Guild", (), {"member_count": 7})()] + guild = type("Guild", (), {"member_count": 7})() + dashboard_cog.bot.get_guild = lambda guild_id: guild await dashboard_cog.on_ready() - mock_emit.assert_called_once_with("member_count", value=7) + mock_emit.assert_any_call("member_count", value=7) + + +@pytest.mark.asyncio +@patch("bot.extensions.dashboard_cog.dachshund.emit") +async def test_on_ready__expect_a_members_refresh_ping(mock_emit, dashboard_cog): + """Verify on_ready pings the members table to refetch its loader.""" + await dashboard_cog.on_ready() + + mock_emit.assert_any_call("members") @pytest.mark.asyncio @patch("bot.extensions.dashboard_cog.dachshund.emit") async def test_on_member_join__expect_reports_member_count(mock_emit, dashboard_cog): """Verify on_member_join reports the current member count.""" - dashboard_cog.bot.guilds = [type("Guild", (), {"member_count": 8})()] + guild = type("Guild", (), {"member_count": 8})() + dashboard_cog.bot.get_guild = lambda guild_id: guild + + await dashboard_cog.on_member_join(object()) + + mock_emit.assert_any_call("member_count", value=8) + +@pytest.mark.asyncio +@patch("bot.extensions.dashboard_cog.dachshund.emit") +async def test_on_member_join__expect_a_members_refresh_ping(mock_emit, dashboard_cog): + """Verify on_member_join pings the members table to refetch its loader.""" await dashboard_cog.on_member_join(object()) - mock_emit.assert_called_once_with("member_count", value=8) + mock_emit.assert_any_call("members") @pytest.mark.asyncio @patch("bot.extensions.dashboard_cog.dachshund.emit") async def test_on_member_remove__expect_reports_member_count(mock_emit, dashboard_cog): """Verify on_member_remove reports the current member count.""" - dashboard_cog.bot.guilds = [type("Guild", (), {"member_count": 6})()] + guild = type("Guild", (), {"member_count": 6})() + dashboard_cog.bot.get_guild = lambda guild_id: guild + + await dashboard_cog.on_member_remove(object()) + mock_emit.assert_any_call("member_count", value=6) + + +@pytest.mark.asyncio +@patch("bot.extensions.dashboard_cog.dachshund.emit") +async def test_on_member_remove__expect_a_members_refresh_ping(mock_emit, dashboard_cog): + """Verify on_member_remove pings the members table to refetch its loader.""" await dashboard_cog.on_member_remove(object()) - mock_emit.assert_called_once_with("member_count", value=6) + mock_emit.assert_any_call("members") @pytest.mark.asyncio diff --git a/tests/lib/dashboard/test_dashboard.py b/tests/lib/dashboard/test_dashboard.py index 53dad80..9673a32 100644 --- a/tests/lib/dashboard/test_dashboard.py +++ b/tests/lib/dashboard/test_dashboard.py @@ -2,7 +2,7 @@ import dachshund -from lib.dashboard.dashboard import build_dashboard +from lib.dashboard.dashboard import build_dashboard, load_members @patch.object(dachshund, "widget") @@ -20,7 +20,12 @@ def test_build_dashboard__expect_all_charts_registered(mock_chart, mock_widget): "command_error", "connection_event", ] - assert widget_names == ["average_latency_gauge", "daily_message_rate"] + assert widget_names == [ + "member_count", + "members", + "average_latency_gauge", + "daily_message_rate", + ] @patch.object(dachshund, "widget") @@ -105,3 +110,57 @@ def test_build_dashboard__with_connection_event_chart__expect_bar_chart_by_type( assert connection_event_call.kwargs["type"] == "bar" assert connection_event_call.kwargs["x"] == "type" assert connection_event_call.kwargs["persist"] is True + + +@patch.object(dachshund, "widget") +@patch.object(dachshund, "chart") +def test_build_dashboard__with_members_table__expect_a_loader(mock_chart, mock_widget): + """Verify the members table fetches its rows through a loader, not a local mirror.""" + build_dashboard() + + members_call = next( + call for call in mock_widget.call_args_list if call.args[0] == "members" + ) + assert members_call.kwargs["type"] == "table" + assert members_call.kwargs["loader"] is load_members + assert "persist" not in members_call.kwargs + + +def _fake_bot(guild): + return type("Bot", (), {"get_guild": staticmethod(lambda guild_id: guild)})() + + +def _fake_member(id, name): + return type("Member", (), {"id": id, "name": name})() + + +def test_load_members__expect_every_member_as_a_row(): + guild = type( + "Guild", + (), + {"id": 1349086642446532639, "name": "Code Society", "members": [_fake_member(1, "ada")]}, + )() + bot = _fake_bot(guild) + + rows, next_cursor = load_members({"bot": bot}, None, 5) + + assert rows == [ + {"id": 1, "name": "ada", "guild": "Code Society", "_guild_id": "1349086642446532639"} + ] + assert next_cursor is None + + +def test_load_members__expect_a_next_cursor_when_more_remain(): + members = [_fake_member(i, f"user{i}") for i in range(3)] + guild = type( + "Guild", (), {"id": 1349086642446532639, "name": "Code Society", "members": members} + )() + bot = _fake_bot(guild) + + first_rows, next_cursor = load_members({"bot": bot}, None, 2) + second_rows, second_cursor = load_members({"bot": bot}, next_cursor, 2) + + assert [row["id"] for row in first_rows] == [0, 1] + assert next_cursor == "2" + assert [row["id"] for row in second_rows] == [2] + assert second_cursor is None From 5fcf1c193b67ff1edf177952a1f906b84b9f909c Mon Sep 17 00:00:00 2001 From: penguinboi Date: Thu, 22 Oct 2026 16:46:21 -0400 Subject: [PATCH 3/7] add summary and moved memebers widget --- bot/extensions/dashboard_cog.py | 11 +++++++++++ bot/grace.py | 5 ++--- lib/dashboard/dashboard.py | 32 ++++++++++++++++++++------------ 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/bot/extensions/dashboard_cog.py b/bot/extensions/dashboard_cog.py index 3a28369..c786e86 100644 --- a/bot/extensions/dashboard_cog.py +++ b/bot/extensions/dashboard_cog.py @@ -11,6 +11,16 @@ from lib.dashboard.dashboard import GUILD_IDS +SUMMARY = """ +# Welcome! +### This dashboard contain various monitoring information about **Grace**, our awesome bot! +Hopefully, you will find something useful here. + +- [Website](https://codesociety.xyz) +- [Discord](https://discord.gg/FzgwHD7Am3) +- [Source](https://github.com/Code-Society-Lab/grace) +""" + class DashboardCog( Cog, name="Dashboard", description="Feeds bot telemetry to the live dashboard" ): @@ -27,6 +37,7 @@ def __init__(self, bot: Grace): def cog_load(self) -> None: self._load_counts() + dachshund.emit("summary", value=SUMMARY) self.jobs.append( self.bot.scheduler.add_job( diff --git a/bot/grace.py b/bot/grace.py index 5aca2f6..8b4c103 100644 --- a/bot/grace.py +++ b/bot/grace.py @@ -59,8 +59,9 @@ async def load_extensions(self): async def setup_hook(self): await super().setup_hook() - dashboard.init_dachshund() + self.metrics = await DashboardMetrics.load() + dashboard.init_dachshund() dashboard.build_dashboard() dashboard.start_dashboard({"bot": self}) @@ -69,5 +70,3 @@ async def on_ready(self): async def on_reload(self): await super().on_reload() - dashboard.init_dachshund() - dashboard.start_dashboard() diff --git a/lib/dashboard/dashboard.py b/lib/dashboard/dashboard.py index 7031f67..f625126 100644 --- a/lib/dashboard/dashboard.py +++ b/lib/dashboard/dashboard.py @@ -57,18 +57,9 @@ def load_members(ctx: dict, cursor: str | None, page_size: int): def build_dashboard() -> None: dachshund.widget( - "member_count", - type="number", - size=1, - ) - - dachshund.widget( - "members", - type="table", - columns=["id", "name", "guild", "_guild_id"], - actions=["greet"], - loader=load_members, - size=2, + "summary", + type="text", + size=3 ) # latency @@ -110,6 +101,23 @@ def build_dashboard() -> None: persist=True, ) + # members + dachshund.widget( + "member_count", + type="number", + size=1, + ) + + dachshund.widget( + "members", + type="table", + columns=["id", "name", "guild", "_guild_id"], + actions=["greet"], + loader=load_members, + size=2, + ) + + # commands dachshund.chart( "on_command", type="bar", From 820efe82c0489aaa0094542ae0a10b17b69c97ca Mon Sep 17 00:00:00 2001 From: penguinboi Date: Sun, 1 Nov 2026 00:49:52 -0400 Subject: [PATCH 4/7] updated dashboard --- bot/extensions/dashboard_cog.py | 2 +- lib/dashboard/dashboard.py | 127 +------------- lib/dashboard/pages/__init__.py | 0 lib/dashboard/pages/activity.py | 25 +++ lib/dashboard/pages/overview.py | 70 ++++++++ lib/dashboard/widgets/gauge.py | 2 +- tests/lib/dashboard/pages/__init__.py | 0 tests/lib/dashboard/pages/test_activity.py | 32 ++++ tests/lib/dashboard/pages/test_overview.py | 76 +++++++++ tests/lib/dashboard/test_dashboard.py | 185 +++------------------ 10 files changed, 236 insertions(+), 283 deletions(-) create mode 100644 lib/dashboard/pages/__init__.py create mode 100644 lib/dashboard/pages/activity.py create mode 100644 lib/dashboard/pages/overview.py create mode 100644 tests/lib/dashboard/pages/__init__.py create mode 100644 tests/lib/dashboard/pages/test_activity.py create mode 100644 tests/lib/dashboard/pages/test_overview.py diff --git a/bot/extensions/dashboard_cog.py b/bot/extensions/dashboard_cog.py index c786e86..be1c536 100644 --- a/bot/extensions/dashboard_cog.py +++ b/bot/extensions/dashboard_cog.py @@ -8,7 +8,7 @@ from bot.grace import Grace from bot.models.extensions.widget_state import WidgetState -from lib.dashboard.dashboard import GUILD_IDS +from lib.dashboard.pages.overview import GUILD_IDS SUMMARY = """ diff --git a/lib/dashboard/dashboard.py b/lib/dashboard/dashboard.py index f625126..5b5f866 100644 --- a/lib/dashboard/dashboard.py +++ b/lib/dashboard/dashboard.py @@ -1,13 +1,11 @@ import dachshund from bot import app +from lib.dashboard.pages.activity import activity_page +from lib.dashboard.pages.overview import overview_page from lib.dashboard.sqlmodel_storage import SQLModelStorage -from lib.dashboard.widgets.gauge import ( - Gauge, -) # registers the "gauge" widget type on import DACHSHUND_EVENT_STORE = app.config.get("dachshund", "event_store", "dachshund.db") -GUILD_IDS: list[int] = [1349086642446532639] def init_dachshund(): @@ -24,124 +22,7 @@ def start_dashboard(context: dict): context=context ) -@dachshund.action("greet", label="Greet") -async def greet_user(ctx: dict, name: str, guild_id: str) -> None: - bot = ctx["bot"] - - if guild := bot.get_guild(int(guild_id)): - await guild.system_channel.send(f"Hello {name}!") - - -def load_members(ctx: dict, cursor: str | None, page_size: int): - bot = ctx["bot"] - members = [] - - for guild_id in GUILD_IDS: - if guild := bot.get_guild(guild_id): - for member in guild.members: - members.append( - { - "id": member.id, - "name": member.name, - "guild": guild.name, - "_guild_id": str(guild.id), - } - ) - - start = int(cursor) if cursor else 0 - rows = members[start : start + page_size] - next_cursor = str(start + page_size) if start + page_size < len(members) else None - - return rows, next_cursor - def build_dashboard() -> None: - dachshund.widget( - "summary", - type="text", - size=3 - ) - - # latency - dachshund.chart( - "latency", - type="timeseries", - title="Live Latency", - y=["check", "command"], - size=2, - ) - - dachshund.widget( - "average_latency_gauge", - type="gauge", - title="Latency Health", - source="average_latency", - min=0, - max=500, - warning_at=150, - critical_at=300, - unit="ms", - size=1, - ) - - # message - dachshund.widget( - "daily_message_rate", - type="number", - title="Today's Messages Rate", - size=1, - ) - - dachshund.chart( - "weekly_message_counts", - type="bar", - title="Messages / day", - x="date", - size=2, - persist=True, - ) - - # members - dachshund.widget( - "member_count", - type="number", - size=1, - ) - - dachshund.widget( - "members", - type="table", - columns=["id", "name", "guild", "_guild_id"], - actions=["greet"], - loader=load_members, - size=2, - ) - - # commands - dachshund.chart( - "on_command", - type="bar", - title="Calls Per Command", - x="name", - size=3, - persist=True, - ) - - dachshund.chart( - "command_error", - type="bar", - title="Errors Per Command", - x="name", - size=3, - persist=True, - ) - - # connection events - dachshund.chart( - "connection_event", - type="bar", - title="Connection Events", - x="type", - size=3, - persist=True, - ) + for page in (overview_page, activity_page): + dachshund.include(page) diff --git a/lib/dashboard/pages/__init__.py b/lib/dashboard/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/dashboard/pages/activity.py b/lib/dashboard/pages/activity.py new file mode 100644 index 0000000..ca5e8f2 --- /dev/null +++ b/lib/dashboard/pages/activity.py @@ -0,0 +1,25 @@ +from dachshund import BarChart, Number, Page + +activity_page = Page("activity", title="Activity") + +messages = activity_page.tab("messages", title="Messages") +message_rate = messages.add(Number("daily_message_rate", title="Today's Messages Rate")) +message_counts = messages.add( + BarChart("weekly_message_counts", title="Messages / day", x="date", persist=True) +) +messages.row(message_rate, message_counts) + +commands = activity_page.tab("commands", title="Commands") +calls = commands.add( + BarChart("on_command", title="Calls Per Command", x="name", persist=True) +) +errors = commands.add( + BarChart("command_error", title="Errors Per Command", x="name", persist=True) +) +commands.row(calls) +commands.row(errors) + +connections = activity_page.tab("connections", title="Connections") +connections.add( + BarChart("connection_event", title="Connection Events", x="type", persist=True) +) diff --git a/lib/dashboard/pages/overview.py b/lib/dashboard/pages/overview.py new file mode 100644 index 0000000..d1b3632 --- /dev/null +++ b/lib/dashboard/pages/overview.py @@ -0,0 +1,70 @@ +from dachshund import Number, Page, Table, Text, TimeSeriesChart + +from lib.dashboard.widgets.gauge import Gauge + +GUILD_IDS: list[int] = [1349086642446532639] + + +def load_members(ctx: dict, cursor: str | None, page_size: int): + bot = ctx["bot"] + members = [] + + for guild_id in GUILD_IDS: + if guild := bot.get_guild(guild_id): + for member in guild.members: + members.append( + { + "id": member.id, + "name": member.name, + "guild": guild.name, + "_guild_id": str(guild.id), + } + ) + + start = int(cursor) if cursor else 0 + rows = members[start : start + page_size] + next_cursor = str(start + page_size) if start + page_size < len(members) else None + + return rows, next_cursor + + +overview_page = Page("overview", title="Overview") + + +@overview_page.action("greet", label="Greet") +async def greet_user(ctx: dict, name: str, guild_id: str) -> None: + bot = ctx["bot"] + + if guild := bot.get_guild(int(guild_id)): + await guild.system_channel.send(f"Hello {name}!") + + +summary = overview_page.add(Text("summary")) +latency = overview_page.add( + TimeSeriesChart("latency", title="Live Latency", y=["check", "command"]) +) +gauge = overview_page.add( + Gauge( + "average_latency_gauge", + title="Latency Health", + source="average_latency", + min=0, + max=500, + warning_at=150, + critical_at=300, + unit="ms", + ) +) +member_count = overview_page.add(Number("member_count")) +members = overview_page.add( + Table( + "members", + columns=["id", "name", "guild", "_guild_id"], + actions=["greet"], + loader=load_members, + ) +) + +overview_page.row(summary) +overview_page.row(latency, overview_page.column(gauge, member_count)) +overview_page.row(members) diff --git a/lib/dashboard/widgets/gauge.py b/lib/dashboard/widgets/gauge.py index 29696f3..fefbf23 100644 --- a/lib/dashboard/widgets/gauge.py +++ b/lib/dashboard/widgets/gauge.py @@ -16,7 +16,7 @@ class Gauge(Widget): ## Example ```python - dachshund.widget("cpu", type="gauge", title="CPU Load", max=100, unit="%") + dachshund.add(Gauge("cpu", title="CPU Load", max=100, unit="%")) dachshund.emit("cpu", value=42) ``` """ diff --git a/tests/lib/dashboard/pages/__init__.py b/tests/lib/dashboard/pages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/lib/dashboard/pages/test_activity.py b/tests/lib/dashboard/pages/test_activity.py new file mode 100644 index 0000000..b8f531a --- /dev/null +++ b/tests/lib/dashboard/pages/test_activity.py @@ -0,0 +1,32 @@ +import pytest +from dachshund import BarChart, Number + +from lib.dashboard.pages.activity import activity_page + + +def test_activity_page__expect_one_tab_per_topic(): + assert list(activity_page.tabs) == ["messages", "commands", "connections"] + + +@pytest.mark.parametrize( + "tab, name, x", + [ + ("messages", "weekly_message_counts", "date"), + ("commands", "on_command", "name"), + ("commands", "command_error", "name"), + ("connections", "connection_event", "type"), + ], +) +def test_activity_page__expect_persisted_bar_charts(tab, name, x): + chart = activity_page.tabs[tab].widgets[name] + + assert isinstance(chart, BarChart) + assert chart.x == x + assert chart.persist is True + + +def test_activity_page__with_messages_tab__expect_rate_beside_counts(): + messages = activity_page.tabs["messages"] + + assert isinstance(messages.widgets["daily_message_rate"], Number) + assert messages.rows == [["daily_message_rate", "weekly_message_counts"]] diff --git a/tests/lib/dashboard/pages/test_overview.py b/tests/lib/dashboard/pages/test_overview.py new file mode 100644 index 0000000..75c9f70 --- /dev/null +++ b/tests/lib/dashboard/pages/test_overview.py @@ -0,0 +1,76 @@ +from dachshund import Column, Table + +from lib.dashboard.pages.overview import greet_user, load_members, overview_page +from lib.dashboard.widgets.gauge import Gauge + + +def test_overview_page__expect_summary_latency_then_members(): + assert overview_page.rows == [ + ["summary"], + ["latency", Column(["average_latency_gauge", "member_count"])], + ["members"], + ] + + +def test_overview_page__with_average_latency_gauge__expect_gauge_widget_in_ms(): + gauge = overview_page.widgets["average_latency_gauge"] + + assert isinstance(gauge, Gauge) + assert gauge.source == "average_latency" + assert gauge.unit == "ms" + + +def test_overview_page__with_members_table__expect_a_loader(): + members = overview_page.widgets["members"] + + assert isinstance(members, Table) + assert members.loader is load_members + assert members.persist is False + assert members.actions == ["greet"] + + +def test_overview_page__expect_the_greet_action(): + action = overview_page.actions["greet"] + + assert action.handler is greet_user + assert action.label == "Greet" + + +def _fake_bot(guild): + return type("Bot", (), {"get_guild": staticmethod(lambda guild_id: guild)})() + + +def _fake_member(id, name): + return type("Member", (), {"id": id, "name": name})() + + +def test_load_members__expect_every_member_as_a_row(): + guild = type( + "Guild", + (), + {"id": 1349086642446532639, "name": "Code Society", "members": [_fake_member(1, "ada")]}, + )() + bot = _fake_bot(guild) + + rows, next_cursor = load_members({"bot": bot}, None, 5) + + assert rows == [ + {"id": 1, "name": "ada", "guild": "Code Society", "_guild_id": "1349086642446532639"} + ] + assert next_cursor is None + + +def test_load_members__expect_a_next_cursor_when_more_remain(): + members = [_fake_member(i, f"user{i}") for i in range(3)] + guild = type( + "Guild", (), {"id": 1349086642446532639, "name": "Code Society", "members": members} + )() + bot = _fake_bot(guild) + + first_rows, next_cursor = load_members({"bot": bot}, None, 2) + second_rows, second_cursor = load_members({"bot": bot}, next_cursor, 2) + + assert [row["id"] for row in first_rows] == [0, 1] + assert next_cursor == "2" + assert [row["id"] for row in second_rows] == [2] + assert second_cursor is None diff --git a/tests/lib/dashboard/test_dashboard.py b/tests/lib/dashboard/test_dashboard.py index 9673a32..e560421 100644 --- a/tests/lib/dashboard/test_dashboard.py +++ b/tests/lib/dashboard/test_dashboard.py @@ -1,166 +1,35 @@ -from unittest.mock import patch - import dachshund +from dachshund import Dashboard -from lib.dashboard.dashboard import build_dashboard, load_members - - -@patch.object(dachshund, "widget") -@patch.object(dachshund, "chart") -def test_build_dashboard__expect_all_charts_registered(mock_chart, mock_widget): - """Verify build_dashboard registers every chart and widget exactly once.""" - build_dashboard() - - chart_names = [call.args[0] for call in mock_chart.call_args_list] - widget_names = [call.args[0] for call in mock_widget.call_args_list] - assert chart_names == [ - "latency", - "weekly_message_counts", - "on_command", - "command_error", - "connection_event", - ] - assert widget_names == [ - "member_count", - "members", - "average_latency_gauge", - "daily_message_rate", - ] - - -@patch.object(dachshund, "widget") -@patch.object(dachshund, "chart") -def test_build_dashboard__with_on_command_chart__expect_bar_chart_by_name( - mock_chart, mock_widget -): - """Verify the on_command chart is configured as a bar chart keyed by command name.""" - build_dashboard() +from lib.dashboard.dashboard import build_dashboard +from lib.dashboard.sqlmodel_storage import SQLModelStorage - on_command_call = next( - call for call in mock_chart.call_args_list if call.args[0] == "on_command" - ) - assert on_command_call.kwargs["type"] == "bar" - assert on_command_call.kwargs["x"] == "name" - assert on_command_call.kwargs["persist"] is True +def test_build_dashboard__expect_every_page_and_widget_included(monkeypatch): + dashboard = Dashboard(path=":memory:", storage=SQLModelStorage()) + monkeypatch.setattr(dachshund, "include", dashboard.include) -@patch.object(dachshund, "widget") -@patch.object(dachshund, "chart") -def test_build_dashboard__with_weekly_message_counts_chart__expect_bar_chart_by_date( - mock_chart, mock_widget -): - """Verify the weekly_message_counts chart is configured as a persisted bar chart keyed by date.""" build_dashboard() - weekly_call = next( - call - for call in mock_chart.call_args_list - if call.args[0] == "weekly_message_counts" - ) - assert weekly_call.kwargs["type"] == "bar" - assert weekly_call.kwargs["x"] == "date" - assert weekly_call.kwargs["persist"] is True - - -@patch.object(dachshund, "widget") -@patch.object(dachshund, "chart") -def test_build_dashboard__with_average_latency_gauge__expect_gauge_widget_in_ms( - mock_chart, mock_widget -): - """Verify the average_latency_gauge widget reads the average_latency source in ms.""" - build_dashboard() - - gauge_call = next( - call - for call in mock_widget.call_args_list - if call.args[0] == "average_latency_gauge" - ) - assert gauge_call.kwargs["type"] == "gauge" - assert gauge_call.kwargs["source"] == "average_latency" - assert gauge_call.kwargs["unit"] == "ms" - - -@patch.object(dachshund, "widget") -@patch.object(dachshund, "chart") -def test_build_dashboard__with_command_error_chart__expect_bar_chart_by_name( - mock_chart, mock_widget -): - """Verify the command_error chart is configured as a persisted bar chart keyed by command name.""" - build_dashboard() - - command_error_call = next( - call for call in mock_chart.call_args_list if call.args[0] == "command_error" - ) - assert command_error_call.kwargs["type"] == "bar" - assert command_error_call.kwargs["x"] == "name" - assert command_error_call.kwargs["persist"] is True - - -@patch.object(dachshund, "widget") -@patch.object(dachshund, "chart") -def test_build_dashboard__with_connection_event_chart__expect_bar_chart_by_type( - mock_chart, mock_widget -): - """Verify the connection_event chart is configured as a persisted bar chart keyed by event type.""" - build_dashboard() - - connection_event_call = next( - call for call in mock_chart.call_args_list if call.args[0] == "connection_event" - ) - assert connection_event_call.kwargs["type"] == "bar" - assert connection_event_call.kwargs["x"] == "type" - assert connection_event_call.kwargs["persist"] is True - - -@patch.object(dachshund, "widget") -@patch.object(dachshund, "chart") -def test_build_dashboard__with_members_table__expect_a_loader(mock_chart, mock_widget): - """Verify the members table fetches its rows through a loader, not a local mirror.""" - build_dashboard() - - members_call = next( - call for call in mock_widget.call_args_list if call.args[0] == "members" - ) - assert members_call.kwargs["type"] == "table" - assert members_call.kwargs["loader"] is load_members - assert "persist" not in members_call.kwargs - - -def _fake_bot(guild): - return type("Bot", (), {"get_guild": staticmethod(lambda guild_id: guild)})() - - -def _fake_member(id, name): - return type("Member", (), {"id": id, "name": name})() - - -def test_load_members__expect_every_member_as_a_row(): - guild = type( - "Guild", - (), - {"id": 1349086642446532639, "name": "Code Society", "members": [_fake_member(1, "ada")]}, - )() - bot = _fake_bot(guild) - - rows, next_cursor = load_members({"bot": bot}, None, 5) - - assert rows == [ - {"id": 1, "name": "ada", "guild": "Code Society", "_guild_id": "1349086642446532639"} + assert list(dashboard.pages) == [ + "overview", + "activity", + "activity/messages", + "activity/commands", + "activity/connections", ] - assert next_cursor is None - - -def test_load_members__expect_a_next_cursor_when_more_remain(): - members = [_fake_member(i, f"user{i}") for i in range(3)] - guild = type( - "Guild", (), {"id": 1349086642446532639, "name": "Code Society", "members": members} - )() - bot = _fake_bot(guild) - - first_rows, next_cursor = load_members({"bot": bot}, None, 2) - second_rows, second_cursor = load_members({"bot": bot}, next_cursor, 2) - - assert [row["id"] for row in first_rows] == [0, 1] - assert next_cursor == "2" - assert [row["id"] for row in second_rows] == [2] - assert second_cursor is None + assert sorted(dashboard.widgets) == sorted( + [ + "summary", + "latency", + "average_latency_gauge", + "daily_message_rate", + "member_count", + "members", + "weekly_message_counts", + "on_command", + "command_error", + "connection_event", + ] + ) + assert list(dashboard.actions) == ["greet"] From 0531590e8f2a74f4cd5818be1bc8c02d4c486fc8 Mon Sep 17 00:00:00 2001 From: penguinboi Date: Sun, 1 Nov 2026 01:37:13 -0500 Subject: [PATCH 5/7] updated dashboard using new layout system --- bot/extensions/dashboard_cog.py | 16 +++---- lib/dashboard/dashboard.py | 2 +- lib/dashboard/pages/activity.py | 22 +++------ lib/dashboard/pages/overview.py | 54 ++++++++++------------ lib/dashboard/widgets/gauge.py | 2 +- tests/extensions/test_dashboard_cog.py | 13 +++--- tests/lib/dashboard/pages/test_activity.py | 31 ++++++------- tests/lib/dashboard/pages/test_overview.py | 26 ++++++++--- tests/lib/dashboard/test_dashboard.py | 8 +--- 9 files changed, 80 insertions(+), 94 deletions(-) diff --git a/bot/extensions/dashboard_cog.py b/bot/extensions/dashboard_cog.py index be1c536..53be5e1 100644 --- a/bot/extensions/dashboard_cog.py +++ b/bot/extensions/dashboard_cog.py @@ -8,7 +8,6 @@ from bot.grace import Grace from bot.models.extensions.widget_state import WidgetState -from lib.dashboard.pages.overview import GUILD_IDS SUMMARY = """ @@ -21,6 +20,7 @@ - [Source](https://github.com/Code-Society-Lab/grace) """ + class DashboardCog( Cog, name="Dashboard", description="Feeds bot telemetry to the live dashboard" ): @@ -40,9 +40,7 @@ def cog_load(self) -> None: dachshund.emit("summary", value=SUMMARY) self.jobs.append( - self.bot.scheduler.add_job( - self._report_latency, "cron", minute="*/1", timezone=self.timezone - ) + self.bot.scheduler.add_job(self._report_latency, "interval", minutes=1) ) self.jobs.append( self.bot.scheduler.add_job( @@ -99,12 +97,10 @@ def _reset_daily_count(self) -> None: self._save_counts() def _report_member_count(self) -> None: - for guild_id in GUILD_IDS: - if guild := self.bot.get_guild(guild_id): - dachshund.emit( - "member_count", - value=guild.member_count, - ) + guild_id = self.bot.app.config.get("client", "guild_id") + + if guild := self.bot.get_guild(guild_id): + dachshund.emit("member_count", value=guild.member_count) @Cog.listener() async def on_ready(self) -> None: diff --git a/lib/dashboard/dashboard.py b/lib/dashboard/dashboard.py index 5b5f866..a28f1e0 100644 --- a/lib/dashboard/dashboard.py +++ b/lib/dashboard/dashboard.py @@ -19,7 +19,7 @@ def start_dashboard(context: dict): "Grace", description="Code Society's Discord Bot", host="0.0.0.0", - context=context + context=context, ) diff --git a/lib/dashboard/pages/activity.py b/lib/dashboard/pages/activity.py index ca5e8f2..4e4ecdb 100644 --- a/lib/dashboard/pages/activity.py +++ b/lib/dashboard/pages/activity.py @@ -2,24 +2,14 @@ activity_page = Page("activity", title="Activity") -messages = activity_page.tab("messages", title="Messages") -message_rate = messages.add(Number("daily_message_rate", title="Today's Messages Rate")) -message_counts = messages.add( - BarChart("weekly_message_counts", title="Messages / day", x="date", persist=True) +activity_page.row( + Number("daily_message_rate", title="Today's Messages Rate"), + BarChart("weekly_message_counts", title="Messages / day", x="date", persist=True), + widths=(1, 2), ) -messages.row(message_rate, message_counts) - -commands = activity_page.tab("commands", title="Commands") -calls = commands.add( +activity_page.row( BarChart("on_command", title="Calls Per Command", x="name", persist=True) ) -errors = commands.add( +activity_page.row( BarChart("command_error", title="Errors Per Command", x="name", persist=True) ) -commands.row(calls) -commands.row(errors) - -connections = activity_page.tab("connections", title="Connections") -connections.add( - BarChart("connection_event", title="Connection Events", x="type", persist=True) -) diff --git a/lib/dashboard/pages/overview.py b/lib/dashboard/pages/overview.py index d1b3632..4039c24 100644 --- a/lib/dashboard/pages/overview.py +++ b/lib/dashboard/pages/overview.py @@ -1,25 +1,25 @@ -from dachshund import Number, Page, Table, Text, TimeSeriesChart +from dachshund import Number, Page, Table, Text, TimeSeriesChart, BarChart +from bot import app from lib.dashboard.widgets.gauge import Gauge -GUILD_IDS: list[int] = [1349086642446532639] +overview_page = Page("overview", title="Overview") def load_members(ctx: dict, cursor: str | None, page_size: int): bot = ctx["bot"] members = [] - for guild_id in GUILD_IDS: - if guild := bot.get_guild(guild_id): - for member in guild.members: - members.append( - { - "id": member.id, - "name": member.name, - "guild": guild.name, - "_guild_id": str(guild.id), - } - ) + if guild := bot.get_guild(app.config.get("client", "guild_id")): + for member in guild.members: + members.append( + { + "id": member.id, + "name": member.name, + "guild": guild.name, + "_guild_id": str(guild.id), + } + ) start = int(cursor) if cursor else 0 rows = members[start : start + page_size] @@ -28,9 +28,6 @@ def load_members(ctx: dict, cursor: str | None, page_size: int): return rows, next_cursor -overview_page = Page("overview", title="Overview") - - @overview_page.action("greet", label="Greet") async def greet_user(ctx: dict, name: str, guild_id: str) -> None: bot = ctx["bot"] @@ -39,11 +36,8 @@ async def greet_user(ctx: dict, name: str, guild_id: str) -> None: await guild.system_channel.send(f"Hello {name}!") -summary = overview_page.add(Text("summary")) -latency = overview_page.add( - TimeSeriesChart("latency", title="Live Latency", y=["check", "command"]) -) -gauge = overview_page.add( +overview_page.row(Text("summary")) +overview_page.row( Gauge( "average_latency_gauge", title="Latency Health", @@ -53,18 +47,20 @@ async def greet_user(ctx: dict, name: str, guild_id: str) -> None: warning_at=150, critical_at=300, unit="ms", - ) + ), + TimeSeriesChart("latency", title="Live Latency", y=["check", "command"]), + widths=(1, 2), ) -member_count = overview_page.add(Number("member_count")) -members = overview_page.add( +overview_page.row( + Number("member_count"), Table( "members", columns=["id", "name", "guild", "_guild_id"], actions=["greet"], loader=load_members, - ) + ), + widths=(1, 2), +) +overview_page.row( + BarChart("connection_event", title="Connection Events", x="type", persist=True) ) - -overview_page.row(summary) -overview_page.row(latency, overview_page.column(gauge, member_count)) -overview_page.row(members) diff --git a/lib/dashboard/widgets/gauge.py b/lib/dashboard/widgets/gauge.py index fefbf23..f9cbb1f 100644 --- a/lib/dashboard/widgets/gauge.py +++ b/lib/dashboard/widgets/gauge.py @@ -16,7 +16,7 @@ class Gauge(Widget): ## Example ```python - dachshund.add(Gauge("cpu", title="CPU Load", max=100, unit="%")) + dachshund.row(Gauge("cpu", title="CPU Load", max=100, unit="%")) dachshund.emit("cpu", value=42) ``` """ diff --git a/tests/extensions/test_dashboard_cog.py b/tests/extensions/test_dashboard_cog.py index ad01284..c3ac981 100644 --- a/tests/extensions/test_dashboard_cog.py +++ b/tests/extensions/test_dashboard_cog.py @@ -43,7 +43,7 @@ def test_reset_daily_count__expect_persists_zeroed_count(dashboard_cog): def test_report_member_count__expect_emits_member_count_for_configured_guild( mock_emit, dashboard_cog ): - """Verify _report_member_count emits the member count for each guild in GUILD_IDS.""" + """Verify _report_member_count emits the member count for the configured guild.""" guild = type("Guild", (), {"member_count": 15})() dashboard_cog.bot.get_guild = lambda guild_id: guild @@ -108,7 +108,9 @@ async def test_on_member_remove__expect_reports_member_count(mock_emit, dashboar @pytest.mark.asyncio @patch("bot.extensions.dashboard_cog.dachshund.emit") -async def test_on_member_remove__expect_a_members_refresh_ping(mock_emit, dashboard_cog): +async def test_on_member_remove__expect_a_members_refresh_ping( + mock_emit, dashboard_cog +): """Verify on_member_remove pings the members table to refetch its loader.""" await dashboard_cog.on_member_remove(object()) @@ -261,14 +263,13 @@ def test_load_counts__expect_emits_restored_counts(mock_emit, dashboard_cog): def test_cog_load__expect_schedules_cron_jobs(dashboard_cog): - """Verify cog_load schedules latency, count-persistence, and day-reset jobs, pinned to the cog's timezone.""" + """Verify cog_load schedules the latency, count-persistence and day-reset jobs.""" dashboard_cog.cog_load() dashboard_cog.bot.scheduler.add_job.assert_any_call( dashboard_cog._report_latency, - "cron", - minute="*/1", - timezone=dashboard_cog.timezone, + "interval", + minutes=1, ) dashboard_cog.bot.scheduler.add_job.assert_any_call( dashboard_cog._save_counts, diff --git a/tests/lib/dashboard/pages/test_activity.py b/tests/lib/dashboard/pages/test_activity.py index b8f531a..7121eaa 100644 --- a/tests/lib/dashboard/pages/test_activity.py +++ b/tests/lib/dashboard/pages/test_activity.py @@ -1,32 +1,29 @@ import pytest -from dachshund import BarChart, Number +from dachshund import BarChart, Number, Row from lib.dashboard.pages.activity import activity_page -def test_activity_page__expect_one_tab_per_topic(): - assert list(activity_page.tabs) == ["messages", "commands", "connections"] +def test_activity_page__expect_messages_beside_their_rate_then_one_chart_per_row(): + assert activity_page.rows == [ + Row(["daily_message_rate", "weekly_message_counts"], (1, 2)), + Row(["on_command"]), + Row(["command_error"]), + ] + assert isinstance(activity_page.widgets["daily_message_rate"], Number) @pytest.mark.parametrize( - "tab, name, x", + "name, x", [ - ("messages", "weekly_message_counts", "date"), - ("commands", "on_command", "name"), - ("commands", "command_error", "name"), - ("connections", "connection_event", "type"), + ("weekly_message_counts", "date"), + ("on_command", "name"), + ("command_error", "name"), ], ) -def test_activity_page__expect_persisted_bar_charts(tab, name, x): - chart = activity_page.tabs[tab].widgets[name] +def test_activity_page__expect_persisted_bar_charts(name, x): + chart = activity_page.widgets[name] assert isinstance(chart, BarChart) assert chart.x == x assert chart.persist is True - - -def test_activity_page__with_messages_tab__expect_rate_beside_counts(): - messages = activity_page.tabs["messages"] - - assert isinstance(messages.widgets["daily_message_rate"], Number) - assert messages.rows == [["daily_message_rate", "weekly_message_counts"]] diff --git a/tests/lib/dashboard/pages/test_overview.py b/tests/lib/dashboard/pages/test_overview.py index 75c9f70..c7e58ea 100644 --- a/tests/lib/dashboard/pages/test_overview.py +++ b/tests/lib/dashboard/pages/test_overview.py @@ -1,4 +1,4 @@ -from dachshund import Column, Table +from dachshund import Row, Table from lib.dashboard.pages.overview import greet_user, load_members, overview_page from lib.dashboard.widgets.gauge import Gauge @@ -6,9 +6,10 @@ def test_overview_page__expect_summary_latency_then_members(): assert overview_page.rows == [ - ["summary"], - ["latency", Column(["average_latency_gauge", "member_count"])], - ["members"], + Row(["summary"]), + Row(["average_latency_gauge", "latency"], (1, 2)), + Row(["member_count", "members"], (1, 2)), + Row(["connection_event"]), ] @@ -48,14 +49,23 @@ def test_load_members__expect_every_member_as_a_row(): guild = type( "Guild", (), - {"id": 1349086642446532639, "name": "Code Society", "members": [_fake_member(1, "ada")]}, + { + "id": 1349086642446532639, + "name": "Code Society", + "members": [_fake_member(1, "ada")], + }, )() bot = _fake_bot(guild) rows, next_cursor = load_members({"bot": bot}, None, 5) assert rows == [ - {"id": 1, "name": "ada", "guild": "Code Society", "_guild_id": "1349086642446532639"} + { + "id": 1, + "name": "ada", + "guild": "Code Society", + "_guild_id": "1349086642446532639", + } ] assert next_cursor is None @@ -63,7 +73,9 @@ def test_load_members__expect_every_member_as_a_row(): def test_load_members__expect_a_next_cursor_when_more_remain(): members = [_fake_member(i, f"user{i}") for i in range(3)] guild = type( - "Guild", (), {"id": 1349086642446532639, "name": "Code Society", "members": members} + "Guild", + (), + {"id": 1349086642446532639, "name": "Code Society", "members": members}, )() bot = _fake_bot(guild) diff --git a/tests/lib/dashboard/test_dashboard.py b/tests/lib/dashboard/test_dashboard.py index e560421..b6ab448 100644 --- a/tests/lib/dashboard/test_dashboard.py +++ b/tests/lib/dashboard/test_dashboard.py @@ -11,13 +11,7 @@ def test_build_dashboard__expect_every_page_and_widget_included(monkeypatch): build_dashboard() - assert list(dashboard.pages) == [ - "overview", - "activity", - "activity/messages", - "activity/commands", - "activity/connections", - ] + assert list(dashboard.pages) == ["overview", "activity"] assert sorted(dashboard.widgets) == sorted( [ "summary", From 07153e77f4404238c4f49d58957ec970373c5d55 Mon Sep 17 00:00:00 2001 From: penguinboi Date: Tue, 3 Nov 2026 01:02:17 -0500 Subject: [PATCH 6/7] remove command and add more info in user table --- bot/extensions/dashboard_cog.py | 1 - lib/dashboard/pages/overview.py | 15 +++------------ pyproject.toml | 3 +-- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/bot/extensions/dashboard_cog.py b/bot/extensions/dashboard_cog.py index 53be5e1..eedd1d5 100644 --- a/bot/extensions/dashboard_cog.py +++ b/bot/extensions/dashboard_cog.py @@ -9,7 +9,6 @@ from bot.grace import Grace from bot.models.extensions.widget_state import WidgetState - SUMMARY = """ # Welcome! ### This dashboard contain various monitoring information about **Grace**, our awesome bot! diff --git a/lib/dashboard/pages/overview.py b/lib/dashboard/pages/overview.py index 4039c24..a09c255 100644 --- a/lib/dashboard/pages/overview.py +++ b/lib/dashboard/pages/overview.py @@ -16,8 +16,8 @@ def load_members(ctx: dict, cursor: str | None, page_size: int): { "id": member.id, "name": member.name, - "guild": guild.name, - "_guild_id": str(guild.id), + "nickname": member.nick, + "joined at": member.joined_at.strftime("%a %d %b %Y"), } ) @@ -28,14 +28,6 @@ def load_members(ctx: dict, cursor: str | None, page_size: int): return rows, next_cursor -@overview_page.action("greet", label="Greet") -async def greet_user(ctx: dict, name: str, guild_id: str) -> None: - bot = ctx["bot"] - - if guild := bot.get_guild(int(guild_id)): - await guild.system_channel.send(f"Hello {name}!") - - overview_page.row(Text("summary")) overview_page.row( Gauge( @@ -55,8 +47,7 @@ async def greet_user(ctx: dict, name: str, guild_id: str) -> None: Number("member_count"), Table( "members", - columns=["id", "name", "guild", "_guild_id"], - actions=["greet"], + columns=["id", "name", "nickname", "joined at"], loader=load_members, ), widths=(1, 2), diff --git a/pyproject.toml b/pyproject.toml index 9527606..7745175 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,8 +28,7 @@ dependencies = [ "googletrans==4.0.0-rc1", "openai==0.26.1", # TODO: point at a tagged release once dachshund stabilizes -# "dachshund @ git+https://github.com/PenguinBoi12/dachshund.git@poc", - "dachshund @ file:///home/penguinboi/Projects/dachshund" + "dachshund @ git+https://github.com/PenguinBoi12/dachshund.git@poc", ] [project.optional-dependencies] From 88b695c4ea98d6a9e9edaf271b43d3062c42800e Mon Sep 17 00:00:00 2001 From: penguinboi Date: Tue, 3 Nov 2026 01:11:48 -0500 Subject: [PATCH 7/7] remove greet_user tests --- tests/lib/dashboard/pages/test_overview.py | 25 ++++++++++------------ tests/lib/dashboard/test_dashboard.py | 2 +- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/lib/dashboard/pages/test_overview.py b/tests/lib/dashboard/pages/test_overview.py index c7e58ea..29503d5 100644 --- a/tests/lib/dashboard/pages/test_overview.py +++ b/tests/lib/dashboard/pages/test_overview.py @@ -1,6 +1,8 @@ +from datetime import datetime + from dachshund import Row, Table -from lib.dashboard.pages.overview import greet_user, load_members, overview_page +from lib.dashboard.pages.overview import load_members, overview_page from lib.dashboard.widgets.gauge import Gauge @@ -27,22 +29,17 @@ def test_overview_page__with_members_table__expect_a_loader(): assert isinstance(members, Table) assert members.loader is load_members assert members.persist is False - assert members.actions == ["greet"] - - -def test_overview_page__expect_the_greet_action(): - action = overview_page.actions["greet"] - - assert action.handler is greet_user - assert action.label == "Greet" + assert members.actions == [] def _fake_bot(guild): return type("Bot", (), {"get_guild": staticmethod(lambda guild_id: guild)})() -def _fake_member(id, name): - return type("Member", (), {"id": id, "name": name})() +def _fake_member(id, name, nick=None, joined_at=datetime(2025, 3, 14)): + return type( + "Member", (), {"id": id, "name": name, "nick": nick, "joined_at": joined_at} + )() def test_load_members__expect_every_member_as_a_row(): @@ -52,7 +49,7 @@ def test_load_members__expect_every_member_as_a_row(): { "id": 1349086642446532639, "name": "Code Society", - "members": [_fake_member(1, "ada")], + "members": [_fake_member(1, "ada", nick="Ada L.")], }, )() bot = _fake_bot(guild) @@ -63,8 +60,8 @@ def test_load_members__expect_every_member_as_a_row(): { "id": 1, "name": "ada", - "guild": "Code Society", - "_guild_id": "1349086642446532639", + "nickname": "Ada L.", + "joined at": "Fri 14 Mar 2025", } ] assert next_cursor is None diff --git a/tests/lib/dashboard/test_dashboard.py b/tests/lib/dashboard/test_dashboard.py index b6ab448..c636a00 100644 --- a/tests/lib/dashboard/test_dashboard.py +++ b/tests/lib/dashboard/test_dashboard.py @@ -26,4 +26,4 @@ def test_build_dashboard__expect_every_page_and_widget_included(monkeypatch): "connection_event", ] ) - assert list(dashboard.actions) == ["greet"] + assert list(dashboard.actions) == []