From 86be5db6ea756c304b0c62bea869906641c347bc Mon Sep 17 00:00:00 2001 From: Anas Rashid Date: Fri, 9 Oct 2026 17:20:02 +0200 Subject: [PATCH] Moderation dashboard (count tiles that jump to each queue; queue items as cards with icon, status and age) and timelines for the moderation activity and the admin record (a coloured dot per kind of action, relative times, filter chips on the record) Co-Authored-By: Claude Opus 5.5 --- web/src/pages/admin/audit.astro | 42 ++++++++++++++++---------- web/src/pages/mod/index.astro | 52 +++++++++++++++++++-------------- web/src/styles/global.css | 35 ++++++++++++++++++++++ 3 files changed, 91 insertions(+), 38 deletions(-) diff --git a/web/src/pages/admin/audit.astro b/web/src/pages/admin/audit.astro index 804ebf26..4e03dd53 100644 --- a/web/src/pages/admin/audit.astro +++ b/web/src/pages/admin/audit.astro @@ -5,7 +5,7 @@ import Icon from '../../components/Icon.astro'; import AdminNav from '../../components/AdminNav.astro'; import { asUser, COOKIE } from '../../lib/auth'; import { ud } from '../../lib/urdu'; -import { ROLE, CONTENT, ACTION as DO } from '../../lib/roles'; +import { ROLE, CONTENT, ACTION as DO, ago } from '../../lib/roles'; const me = Astro.locals.user; if (!me) return Astro.redirect('/signin?next=/admin/audit'); @@ -18,25 +18,28 @@ const ACTION: Record = { grant: 'اجازت دی', revoke: 'اجازت واپس لی', }; const when = (d: string) => ud(new Date(d).toISOString().slice(0, 16).replace('T', ' ')); +const TONE: Record = { grant: 'ok', enable: 'ok', role: 'action', promote: 'action', 'password-reset': 'accent', disable: 'danger', delete: 'danger', revoke: 'danger' }; +const detail = (e: any) => e.detail?.to ? `${ROLE[e.detail.from] ?? '—'} ← ${ROLE[e.detail.to] ?? e.detail.to}`.replace('— ← ', '') + : e.detail?.content ? [e.detail.target, e.detail.content.map((c: string) => CONTENT[c]).join('، '), e.detail.actions.map((a: string) => DO[a]).join('، ')].filter(Boolean).join(' · ') : ''; +const kinds = [...new Set(log.entries.map((e: any) => e.action as string))]; ---

ایڈمن

- - - - {log.entries.map((e: any) => ( - - - - - - - - ))} - -
وقت (UTC)ایڈمنعملصارفتفصیل
{when(e.at)}{e.actor_email}{ACTION[e.action] ?? e.action}{e.target_email ?? '—'}{e.detail?.to ? `${ROLE[e.detail.from] ?? '—'} ← ${ROLE[e.detail.to] ?? e.detail.to}`.replace('— ← ', '') - : e.detail?.content ? `${e.detail.target ?? ''} · ${e.detail.content.map((c: string) => CONTENT[c]).join('، ')} · ${e.detail.actions.map((a: string) => DO[a]).join('، ')}` : ''}
+ {kinds.length > 1 && ( +
+ {kinds.map((k) => )} +
+ )} +
    + {log.entries.map((e: any) => ( +
  1. +
    {e.actor_email}
    +

    {ACTION[e.action] ?? e.action}{e.target_email && <>: {e.target_email}}

    + {detail(e) &&

    {detail(e)}

    } +
  2. + ))} +
{pages > 1 && ( )} + diff --git a/web/src/pages/mod/index.astro b/web/src/pages/mod/index.astro index 4069bd82..48a486d1 100644 --- a/web/src/pages/mod/index.astro +++ b/web/src/pages/mod/index.astro @@ -5,6 +5,8 @@ import ModNav from '../../components/ModNav.astro'; import { asUser, COOKIE } from '../../lib/auth'; import { STATUS, EVENT, when } from '../../lib/mod'; import { ud } from '../../lib/urdu'; +import { ago } from '../../lib/roles'; +import Icon from '../../components/Icon.astro'; const me = Astro.locals.user; if (!me) return Astro.redirect('/signin?next=/mod'); @@ -14,6 +16,9 @@ const queue = showLog ? null : (await asUser(token, '/api/mod/queue')).data; const log = showLog ? (await asUser(token, '/api/mod/log')).data : null; // what kind of change a row is (a work's text needs no label) const KIND: Record = { order: 'ترتیب: ', 'tags-category': 'ٹیگ: ', 'tags-work': 'ٹیگ: ', ebook: 'ای بک: ' }; +// each kind of change has its icon; each event its tone on the timeline +const ICON: Record = { order: 'arrange', 'tags-category': 'tag', 'tags-work': 'tag', ebook: 'upload' }; +const TONE: Record = { published: 'ok', approved: 'action', submitted: 'accent', returned: 'danger', rejected: 'danger' }; const groups = queue ? [ ['میرے مسودے', queue.mine, 'ابھی کوئی مسودہ نہیں۔ کسی کلام کے صفحے پر "ترمیم کریں" سے شروع کریں۔'], ...(me.role !== 'mod-l2' ? [['جائزے کے منتظر', queue.review, 'جائزے کے لیے کچھ نہیں۔']] : []), @@ -24,36 +29,39 @@ const groups = queue ? [

موڈریشن

{Astro.url.searchParams.has('unchanged') &&

متن میں کوئی تبدیلی نہیں تھی، اس لیے مسودہ نہیں رکھا گیا۔

} - {groups.map(([title, items, empty]) => ( -
+ {queue && ( + + )} + {groups.map(([title, items, empty], i) => ( +

{title} {ud(items.length)}

- {items.length === 0 ?

{empty}

: ( -
))} {log && ( - - - - {log.entries.map((e: any) => ( - - - - - - - - ))} - -
وقت (UTC)کس نےکیاکلامتبصرہ
{when(e.at)}{e.actor_email}{EVENT[e.action] ?? e.action}{e.version && e.action === 'published' ? ` (ورژن ${ud(e.version)})` : ''}{KIND[e.entity] ?? ''}{e.title}{e.comment}
+
    + {log.entries.map((e: any) => ( +
  1. +
    {e.actor_email}
    +

    {EVENT[e.action] ?? e.action}{e.version && e.action === 'published' ? ` (ورژن ${ud(e.version)})` : ''}: + {KIND[e.entity] ?? ''}{e.title}

    + {e.comment &&
    {e.comment}
    } +
  2. + ))} +
)} diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 9811779f..2f2226a5 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -741,3 +741,38 @@ p, li { text-wrap: pretty; } .couplet:hover .sh, .poem .para:hover .sh, .poem .single:hover .sh, .sh:focus-visible { opacity: 1; } @media (hover: none) { .sh { opacity: .55; } } @media (max-width: 700px) { .sh { inset-inline-end: -4px; top: 52px; } } + +/* moderation dashboard: count tiles that jump to their queue; queue items as cards */ +.stat-tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; max-width: 760px; margin: 0 auto 18px; } +.stat { display: flex; flex-direction: column; align-items: center; padding: 12px 10px 8px; border: 1.5px solid var(--border); border-radius: var(--r-lg); + background: var(--surface-raised); color: var(--text); transition: transform .2s, border-color .2s; } +.stat:hover { transform: translateY(-2px); border-color: var(--active); color: var(--text); } +.stat strong { font: 700 2.2rem/1.4 var(--font-ui); color: var(--action); } +.stat span { font-size: var(--fs-sm); color: var(--text-muted); } +.queue { max-width: 760px; margin: 0 auto 22px; scroll-margin-top: 16px; } +.queue h2 { text-align: start; font-size: var(--fs-lg); } +.queue h2::after { display: none; } +.queue-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; } +.queue-card { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--r-md); + background: var(--surface-raised); color: var(--text); transition: transform .2s, border-color .2s; } +.queue-card:hover { transform: translateY(-1px); border-color: var(--active); color: var(--text); } +.queue-card .kind { flex: none; display: grid; place-items: center; width: 34px; height: 34px; border-radius: 50%; background: var(--accent-soft); color: var(--accent); } +.queue-card .what { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.6; } +.queue-card .what small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-xs); } +@media (max-width: 560px) { .queue-card { flex-wrap: wrap; } .queue-card .status { margin-inline-start: 46px; } } +/* an empty queue or list: a quiet line with the ✤ */ +.empty { text-align: center; color: var(--text-muted); padding: 14px; border: 1px dashed var(--line); border-radius: var(--r-md); } +.empty span { display: block; color: var(--brand); opacity: .6; font-size: 1.3rem; } +/* timelines (moderation activity, admin record): a dot per entry, coloured by what happened */ +.timeline { list-style: none; max-width: 760px; margin: 8px auto; padding: 0; padding-inline-start: 22px; border-inline-start: 2px solid var(--line); } +.timeline li { position: relative; margin: 0 0 12px; padding: 8px 14px; border-radius: var(--r-md); background: var(--surface-raised); border: 1px solid var(--line); } +.timeline li::before { content: ''; position: absolute; inset-inline-start: -30px; top: 16px; width: 12px; height: 12px; border-radius: 50%; + background: var(--tone); box-shadow: 0 0 0 3px var(--surface-page); } +.timeline li p { margin: 0; line-height: 1.8; } +.timeline .who { font: var(--fs-sm) var(--font-latin); color: var(--text-muted); } +.timeline header { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; } +.timeline time { font-size: var(--fs-xs); white-space: nowrap; } +.timeline .target { font-family: var(--font-latin); } +.timeline blockquote { margin: 4px 0; padding-inline-start: 10px; border-inline-start: 2px solid var(--tone); color: var(--text-muted); font-size: var(--fs-sm); } +[data-tone] { --tone: var(--text-muted); } +[data-tone='ok'] { --tone: var(--ok); } [data-tone='action'] { --tone: var(--action); } [data-tone='accent'] { --tone: var(--accent); } [data-tone='danger'] { --tone: var(--danger); }