GhostWipe report: chronological order, mobile layout, Ruby branding

Messages within each channel now sort oldest-first (history()/purge()
return newest-first, which read backwards). Added a real mobile layout:
below 700px the sidebar stacks above the message view and its channel
list becomes a horizontally-scrolling chip strip instead of a cramped
vertical column. Report title/footer and the Discord log embeds now
carry Ruby branding alongside the GhostWipe module name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Rosie Val 2026-07-28 18:41:36 +01:00
parent 039e85d65a
commit fdf41cfcbc
2 changed files with 41 additions and 7 deletions

View file

@ -309,6 +309,9 @@ class GhostWipe(commands.Cog):
"messages": [],
}
# channel.history()/purge() return newest-first; the report reads far
# more naturally in actual conversation order.
collected.sort(key=lambda m: m.created_at)
messages = [self._serialize_message(m) for m in collected]
return {
"id": str(channel.id),
@ -541,7 +544,7 @@ class GhostWipe(commands.Cog):
if top:
breakdown = "\n".join(f"**{c['name']}** — {c['message_count']}" for c in top)
embed.add_field(name="Per-Channel Breakdown", value=breakdown, inline=False)
embed.set_footer(text=f"GhostWipe v{self.__version__}")
embed.set_footer(text=f"Ruby · GhostWipe v{self.__version__}")
file_obj = None
if conf["attach_html_report"]:
@ -903,7 +906,7 @@ class GhostWipe(commands.Cog):
f"{humanize_number(stats.get('total_messages_deleted', 0))} messages deleted",
inline=False,
)
embed.set_footer(text=f"GhostWipe v{self.__version__}")
embed.set_footer(text=f"Ruby · GhostWipe v{self.__version__}")
await ctx.send(embed=embed)
@ghostwipe.command(name="history")

View file

@ -33,6 +33,7 @@ STYLE = r"""
--danger: #f04747;
--warn: #faa61a;
--ok: #3ba55d;
--ruby: #e0115f;
--radius: 10px;
--shadow: 0 8px 24px rgba(0,0,0,0.35);
}
@ -171,12 +172,42 @@ html, body {
padding: 8px 20px; border-top: 1px solid var(--border); color: var(--text-faint);
font-size: 11px; text-align: center; flex-shrink: 0;
}
.brand-ruby { color: var(--ruby); font-weight: 700; }
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 6px; }
::-webkit-scrollbar-track { background: transparent; }
@media (max-width: 720px) {
#sidebar { width: 200px; }
#header .stats { gap: 12px; }
@media (max-width: 700px) {
html, body { font-size: 15px; }
#header { padding: 12px 14px; gap: 10px; }
.avatar, .avatar-fallback { width: 40px; height: 40px; }
#header .username { font-size: 14px; }
#header .id-block { min-width: 0; }
#header .stats { order: 3; width: 100%; margin-left: 0; justify-content: space-around; gap: 10px; }
#header .stat .num { font-size: 16px; }
#header .meta-row { order: 4; }
/* Stack sidebar above main content, and turn the channel list into a
horizontally-scrolling strip of chips instead of a tall vertical list —
the classic mobile pattern for a channel/tab picker. */
#body { flex-direction: column; }
#sidebar {
width: 100%; max-height: none;
border-right: none; border-bottom: 1px solid var(--border);
}
#sidebar .sidebar-title { padding: 10px 14px 4px; }
#channel-list {
display: flex; flex-direction: row; flex-wrap: nowrap;
overflow-x: auto; -webkit-overflow-scrolling: touch;
gap: 6px; padding: 4px 10px;
}
.ch-item { flex: 0 0 auto; margin-bottom: 0; }
.ch-item .ch-name { white-space: nowrap; }
#search-wrap { padding: 8px 10px; }
#main { width: 100%; }
.panel-header { padding: 10px 14px; }
.panel-body { padding: 12px 14px; }
.attachment-img { max-width: 100%; }
}
"""
@ -398,7 +429,7 @@ def generate_report_html(
main_content = '<div class="empty-state">Nothing was deleted for this event.</div>'
sidebar_content = '<div class="empty-state" style="padding:20px 10px;">No channels affected.</div>'
title = f"GhostWipe Report — {html_lib.escape(member_name)}"
title = f"Ruby — GhostWipe Report — {html_lib.escape(member_name)}"
doc = f"""<!doctype html>
<html lang="en">
@ -442,7 +473,7 @@ def generate_report_html(
{main_content}
</div>
</div>
<div id="footer">Generated by GhostWipe {html_lib.escape(version)} &middot; {html_lib.escape(generated_at or event_time_str)}</div>
<div id="footer"><span class="brand-ruby">Ruby</span> &middot; GhostWipe v{html_lib.escape(version)} &middot; {html_lib.escape(generated_at or event_time_str)}</div>
</div>
<script>{SEARCH_SCRIPT}</script>
</body>