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 %}&ndash;{% 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 %}