git / moonbite-network / templates/discussions_mod.html · raw
{% extends "base.html" %}{% import "partials/forum_macros.html" as fm with context %}{% block body_class %}{% endblock %}{% block title %}Moderator desk — MoonBite Discussions{% endblock %}{% block meta_description %}Moderator tools for the MoonBite forum.{% endblock %}{% block head_extra %}<meta name="robots" content="noindex, nofollow">{% include "partials/forum_style.html" %}<style nonce="{{ csp_nonce }}"> /* Moderator desk: reports, growth numbers, newest posts. */ .md-main { padding: 40px 0 96px; } .md-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 32px; } .md-head h1 { margin: 8px 0 0; font: 800 clamp(28px, 3.4vw, 40px)/1.1 var(--ds-sans); letter-spacing: -.02em; color: var(--ds-bone); } .md-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-bottom: 40px; } .md-stat { border: 1px solid var(--ds-line); border-radius: 10px; background: var(--ds-slate); padding: 18px 20px; } .md-stat .k { font: 400 11px var(--ds-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--ds-ash); } .md-stat .v { margin-top: 8px; font: 400 30px/1 var(--ds-mono); font-variant-numeric: tabular-nums; color: var(--ds-bone); } .md-stat .d { margin-top: 6px; font-size: 13px; color: var(--ds-ash); } .md-stat .up { color: var(--ds-gain); } .md-stat .down { color: var(--ds-loss); } .md-sec { margin-top: 40px; } .md-sec h2 { margin: 0 0 12px; font-size: 18px; font-weight: 700; color: var(--ds-bone); } .md-sec h2 small { font-weight: 400; color: var(--ds-ash); font-size: 14px; margin-left: 8px; } .md-list { border: 1px solid var(--ds-line); border-radius: 10px; background: var(--ds-slate); } .md-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px 24px; align-items: center; padding: 14px 18px; border-bottom: 1px solid var(--ds-line); } .md-row:last-child { border-bottom: 0; } .md-row .t { font-weight: 700; color: var(--ds-bone); overflow-wrap: anywhere; } .md-row .t a:hover { text-decoration: underline; } .md-row .x { font-size: 14px; color: var(--ds-ash); margin-top: 4px; overflow-wrap: anywhere; } .md-row .m { font: 400 12px var(--ds-mono); color: var(--ds-ash); margin-top: 6px; display: flex; flex-wrap: wrap; gap: 6px 12px; } .md-row .acts { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; } .md-row.gone { opacity: .55; } .md-tag { font: 400 11px var(--ds-mono); padding: 1px 6px; border-radius: 4px; border: 1px solid var(--ds-line-2); color: var(--ds-ash); } .md-tag.hot { border-color: rgba(200, 64, 47, .6); color: var(--ds-bone); } .md-empty { padding: 24px 18px; color: var(--ds-ash); } .md-login { max-width: 420px; margin: 64px auto; } @media (max-width: 767px) { .md-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .md-row { grid-template-columns: minmax(0, 1fr); } .md-row .acts { justify-content: flex-start; } }</style>{% endblock %}{% block content %}<div class="ds"><div class="md-main"><div class="ds-wrap">{% if disabled %} <div class="md-login fx-panel"> <h2>Moderation is off</h2> <p>Set <code>MOONBITE_FORUM_MOD_KEY</code> on the server to enable the moderator desk.</p> </div>{% elif login %} <form class="md-login fx-panel fx-form" method="post" action="{{ url_for('discussions_mod_login') }}"> <h2>Moderator login</h2> {% if error %}<div class="fx-alert" role="alert">{{ error }}</div>{% endif %} <div class="ds-field"> <label for="mod-key">Moderator key</label> <input class="ds-input mono" type="password" id="mod-key" name="key" autocomplete="current-password" required> </div> <button type="submit" class="ds-btn gold">Log in</button> <p class="hint">Five tries every five minutes.</p> </form>{% else %} {% set tw, lw = metrics.this_week, metrics.last_week %} {% macro delta(a, b) -%} {%- if b == 0 and a == 0 %}no change{% elif b == 0 %}<span class="up">new this week</span>{% else %}{% set pct = ((a - b) / b * 100)|round|int %}<span class="{{ 'up' if pct >= 0 else 'down' }}">{{ '+' if pct >= 0 }}{{ pct }}%</span> vs last week ({{ b }}){% endif -%} {%- endmacro %} <div class="md-head"> <div> <ol class="ds-crumbs"><li><a href="{{ url_for('discussions_page') }}">Discussions</a></li><li><span aria-current="page">Moderator desk</span></li></ol> <h1>Moderator desk</h1> </div> <form method="post" action="{{ url_for('discussions_mod_logout') }}"> <input type="hidden" name="csrf" value="{{ forum_csrf }}"> <button type="submit" class="ds-btn sm">Log out</button> </form> </div> {% if request.args.get('error') %}<div class="fx-alert" role="alert">{{ request.args.get('error') }}</div>{% endif %} <p class="ds-eyebrow" style="margin-bottom: 12px;">Last 7 days, people only (verified posts not counted)</p> <div class="md-grid"> <div class="md-stat"><div class="k">People posting</div><div class="v">{{ tw.posters }}</div><div class="d">{{ delta(tw.posters, lw.posters) }}</div></div> <div class="md-stat"><div class="k">New threads</div><div class="v">{{ tw.threads }}</div><div class="d">{{ delta(tw.threads, lw.threads) }}</div></div> <div class="md-stat"><div class="k">Replies</div><div class="v">{{ tw.replies }}</div><div class="d">{{ delta(tw.replies, lw.replies) }}</div></div> {% set a, n = metrics.answered_24h %} <div class="md-stat"><div class="k">Answered in 24h</div><div class="v">{% if n %}{{ (a / n * 100)|round|int }}%{% else %}–{% endif %}</div><div class="d">{{ a }} of {{ n }} threads, last 30 days. Aim for 100%.</div></div> </div> <section class="md-sec" aria-labelledby="rep-h"> <h2 id="rep-h">Reports <small>{{ reports|length }} open</small></h2> <div class="md-list"> {% for r in reports %} {% set link = url_for('discussion_thread_page', thread_id=r.thread_id) ~ ('#c-' ~ r.id if r.kind == 'r' else '') %} <div class="md-row{{ ' gone' if r.removed }}"> <div> <div class="t"><a href="{{ link }}">{{ r.title }}</a>{% if r.kind == 'r' %} <span class="md-tag">comment</span>{% endif %}</div> <div class="x">{{ r.author }}: {{ r.excerpt }}</div> <div class="m"><span class="md-tag{{ ' hot' if r.count > 1 }}">{{ r.count }} report{{ '' if r.count == 1 else 's' }}</span>{% for why in r.reasons %}<span>{{ reasons[why] }}</span>{% endfor %}<span>{{ r.last_ago }}</span></div> </div> <div class="acts"> {% if not r.removed %}{{ fm.mod_button(r.kind, r.id, 'remove', 'Remove', url_for('discussions_mod_page')) }}{% endif %} {{ fm.mod_button(r.kind, r.id, 'dismiss', 'Keep it', url_for('discussions_mod_page')) }} </div> </div> {% else %} <p class="md-empty">No open reports.</p> {% endfor %} </div> </section> <section class="md-sec" aria-labelledby="wait-h"> <h2 id="wait-h">Waiting for a first reply <small>{{ metrics.unanswered|length }}</small></h2> <div class="md-list"> {% for w in metrics.unanswered %} <div class="md-row"><div><div class="t"><a href="{{ url_for('discussion_thread_page', thread_id=w.id) }}#reply">{{ w.title }}</a></div><div class="m"><span>asked {{ fm.ago(w.ago) }}</span></div></div></div> {% else %} <p class="md-empty">Every thread has a reply.</p> {% endfor %} </div> </section> <section class="md-sec" aria-labelledby="new-h"> <h2 id="new-h">Newest posts</h2> <div class="md-list"> {% for p in recent %} {% set link = url_for('discussion_thread_page', thread_id=p.thread_id) ~ ('#c-' ~ p.id if p.kind == 'r' else '') %} <div class="md-row{{ ' gone' if p.removed }}"> <div> <div class="t"><a href="{{ link }}">{{ p.title }}</a>{% if p.kind == 'r' %} <span class="md-tag">comment</span>{% endif %}{% if p.removed %} <span class="md-tag">removed</span>{% endif %}</div> <div class="x">{{ p.author }}{% if p.is_official %} (verified){% endif %}: {{ p.excerpt }}</div> <div class="m"><span>{{ fm.ago(p.ago) }}</span></div> </div> <div class="acts">{{ fm.mod_button(p.kind, p.id, 'restore' if p.removed else 'remove', 'Restore' if p.removed else 'Remove', url_for('discussions_mod_page')) }}</div> </div> {% else %} <p class="md-empty">Nothing posted yet.</p> {% endfor %} </div> </section>{% endif %}</div></div></div>{% endblock %}