06/16 Phase 6 (continue)

This commit is contained in:
2026-06-17 17:06:43 -04:00
parent 479afde2fa
commit 68a842d6b6
26 changed files with 1027 additions and 61 deletions
+5
View File
@@ -3,4 +3,9 @@
<a href="{{ url_for('admin.users') }}" class="{{ 'on' if request.endpoint in ('admin.users', 'admin.user_detail') else '' }}">{{ _('Users') }}</a>
<a href="{{ url_for('admin.listings') }}" class="{{ 'on' if request.endpoint == 'admin.listings' else '' }}">{{ _('Listings') }}</a>
<a href="{{ url_for('admin.reports') }}" class="{{ 'on' if request.endpoint == 'admin.reports' else '' }}">{{ _('Reports') }}</a>
<a href="{{ url_for('admin.categories') }}" class="{{ 'on' if request.endpoint in ('admin.categories', 'admin.toggle_category', 'admin.category_schema') else '' }}">{{ _('Categories') }}</a>
<a href="{{ url_for('admin.plans') }}" class="{{ 'on' if request.endpoint in ('admin.plans', 'admin.plan_edit') else '' }}">{{ _('Plans') }}</a>
<a href="{{ url_for('admin.transactions') }}" class="{{ 'on' if request.endpoint == 'admin.transactions' else '' }}">{{ _('Transactions') }}</a>
<a href="{{ url_for('admin.audit_log') }}" class="{{ 'on' if request.endpoint == 'admin.audit_log' else '' }}">{{ _('Audit log') }}</a>
<a href="{{ url_for('admin.settings') }}" class="{{ 'on' if request.endpoint == 'admin.settings' else '' }}">{{ _('Settings') }}</a>
</nav>
+48
View File
@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Audit log') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Audit log') }}</h2>
<form method="get" class="admin-filter-row">
<div class="field">
<label>{{ _('Action filter') }}</label>
<input class="input" name="action" value="{{ filters.get('action', '') }}" placeholder="e.g. listing.removed">
</div>
<button class="btn ghost" type="submit">{{ _('Filter') }}</button>
{% if filters.get('action') %}<a class="btn ghost" href="{{ url_for('admin.audit_log') }}">{{ _('Clear') }}</a>{% endif %}
</form>
<table class="list">
<thead><tr>
<th>{{ _('When') }}</th>
<th>{{ _('Actor') }}</th>
<th>{{ _('Action') }}</th>
<th>{{ _('Target') }}</th>
<th>{{ _('Meta') }}</th>
</tr></thead>
{% for entry in pagination.items %}
<tr>
<td class="muted small">{{ entry.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
{% if entry.actor %}
<a href="{{ url_for('admin.user_detail', user_id=entry.actor_id) }}">{{ entry.actor.display_name }}</a>
{% else %}<span class="muted"></span>{% endif %}
</td>
<td><code>{{ entry.action }}</code></td>
<td class="muted small">{{ entry.target_type }}{% if entry.target_id %}:{{ entry.target_id }}{% endif %}</td>
<td class="muted small">{{ entry.meta or '' }}</td>
</tr>
{% else %}
<tr><td colspan="5" class="muted">{{ _('No entries.') }}</td></tr>
{% endfor %}
</table>
<div class="pager">
{% if pagination.has_prev %}<a href="{{ url_for('admin.audit_log', **merge_query(page=pagination.prev_num)) }}">&larr; {{ _('Prev') }}</a>{% endif %}
<span class="muted">{{ _('Page %(p)s of %(t)s', p=pagination.page, t=pagination.pages) }}</span>
{% if pagination.has_next %}<a href="{{ url_for('admin.audit_log', **merge_query(page=pagination.next_num)) }}">{{ _('Next') }} &rarr;</a>{% endif %}
</div>
</div>
{% endblock %}
+38
View File
@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Categories') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Categories') }}</h2>
<table class="list">
{% for cat in categories %}
<tr>
<td><strong>{{ cat.name }}</strong> <code class="muted small">{{ cat.slug }}</code></td>
<td><span class="badge {{ 'ok' if cat.is_active else 'warn' }}">{{ _('active') if cat.is_active else _('inactive') }}</span></td>
<td class="muted small">{{ cat.children|length }} {{ _('subcategories') }}</td>
<td>
<a class="btn ghost tiny" href="{{ url_for('admin.category_schema', cat_id=cat.id) }}">{{ _('Field schema') }}</a>
<form method="post" action="{{ url_for('admin.toggle_category', cat_id=cat.id) }}" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn ghost tiny" type="submit">{{ _('Disable') if cat.is_active else _('Enable') }}</button>
</form>
</td>
</tr>
{% for sub in cat.children|sort(attribute='sort_order') %}
<tr style="background:var(--bg2)">
<td style="padding-left:24px">{{ sub.name }} <code class="muted small">{{ sub.slug }}</code></td>
<td><span class="badge {{ 'ok' if sub.is_active else 'warn' }}">{{ _('active') if sub.is_active else _('inactive') }}</span></td>
<td></td>
<td>
<a class="btn ghost tiny" href="{{ url_for('admin.category_schema', cat_id=sub.id) }}">{{ _('Field schema') }}</a>
<form method="post" action="{{ url_for('admin.toggle_category', cat_id=sub.id) }}" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button class="btn ghost tiny" type="submit">{{ _('Disable') if sub.is_active else _('Enable') }}</button>
</form>
</td>
</tr>
{% endfor %}
{% endfor %}
</table>
</div>
{% endblock %}
+18
View File
@@ -0,0 +1,18 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · %(name)s schema', name=cat.name) }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Field schema: %(name)s', name=cat.name) }}</h2>
<p class="muted">{{ _('Edit the JSON field schema for this category. Supported types: text, number, select, bool. Add "hot": true to denormalize onto an indexed column.') }}</p>
{% if error %}<p class="flash danger">{{ error }}</p>{% endif %}
<form method="post" action="{{ url_for('admin.category_schema', cat_id=cat.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="field">
<textarea class="input" name="field_schema" rows="20" style="font-family:monospace;font-size:13px">{{ schema_str }}</textarea>
</div>
<button class="btn" type="submit">{{ _('Save schema') }}</button>
<a class="btn ghost" href="{{ url_for('admin.categories') }}">{{ _('Cancel') }}</a>
</form>
</div>
{% endblock %}
+4 -13
View File
@@ -3,19 +3,10 @@
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Moderation settings') }}</h2>
<form method="post" action="{{ url_for('admin.listings_settings') }}" class="settings-panel">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="field">
<label>{{ _('Auto-flag threshold (distinct reports)') }}</label>
<input class="input" type="number" name="flag_threshold" value="{{ flag_threshold }}" min="1">
</div>
<div class="field">
<label>{{ _('Keyword blocklist (one per line)') }}</label>
<textarea class="input" name="keyword_blocklist">{{ keyword_blocklist|join('\n') }}</textarea>
</div>
<button class="btn" type="submit">{{ _('Save settings') }}</button>
</form>
<p class="muted">
{{ _('Auto-flag threshold: %(n)s distinct reports · Blocklist: %(k)s keywords', n=flag_threshold, k=keyword_blocklist|length) }}
· <a href="{{ url_for('admin.settings') }}">{{ _('Edit in Settings') }}</a>
</p>
<h2>{{ _('Flag queue') }}</h2>
{% if not pagination.items %}<p class="muted">{{ _('No flagged listings.') }}</p>{% endif %}
+30
View File
@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Edit plan: %(slug)s', slug=plan.slug) }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Edit plan: %(name)s', name=plan.name) }}</h2>
{% if error %}<p class="flash danger">{{ error }}</p>{% endif %}
<form method="post" action="{{ url_for('admin.plan_edit', plan_id=plan.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="field">
<label>{{ _('Name') }}</label>
<input class="input" name="name" value="{{ plan.name }}" required>
</div>
<div class="field">
<label>{{ _('Price (cents per month — integer, e.g. 999 = $9.99)') }}</label>
<input class="input" type="number" name="price_monthly_cents" value="{{ plan.price_monthly_cents }}" min="0" required>
</div>
<div class="field">
<label>{{ _('Stripe price ID') }}</label>
<input class="input" name="stripe_price_id" value="{{ plan.stripe_price_id or '' }}" placeholder="price_xxx">
</div>
<div class="field">
<label>{{ _('Config JSON (limits)') }}</label>
<textarea class="input" name="config" rows="20" style="font-family:monospace;font-size:13px">{{ config_str }}</textarea>
</div>
<button class="btn" type="submit">{{ _('Save plan') }}</button>
<a class="btn ghost" href="{{ url_for('admin.plans') }}">{{ _('Cancel') }}</a>
</form>
</div>
{% endblock %}
+28
View File
@@ -0,0 +1,28 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Plans') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Plans') }}</h2>
<table class="list">
<thead><tr>
<th>{{ _('Slug') }}</th>
<th>{{ _('Name') }}</th>
<th>{{ _('Price / mo') }}</th>
<th>{{ _('Stripe price ID') }}</th>
<th>{{ _('Active') }}</th>
<th></th>
</tr></thead>
{% for plan in plans %}
<tr>
<td><code>{{ plan.slug }}</code></td>
<td>{{ plan.name }}</td>
<td>${{ '%.2f'|format(plan.price_monthly_cents / 100) }}</td>
<td class="muted small">{{ plan.stripe_price_id or '—' }}</td>
<td><span class="badge {{ 'ok' if plan.is_active else 'warn' }}">{{ _('yes') if plan.is_active else _('no') }}</span></td>
<td><a class="btn ghost tiny" href="{{ url_for('admin.plan_edit', plan_id=plan.id) }}">{{ _('Edit') }}</a></td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
+43
View File
@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Settings') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Settings') }}</h2>
<form method="post" action="{{ url_for('admin.settings') }}" class="settings-panel">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="check">
<input type="checkbox" name="registration_open" id="registration_open" {{ 'checked' if values.registration_open }}>
<label for="registration_open">{{ _('Registration open') }}</label>
</div>
<div class="check">
<input type="checkbox" name="ads_enabled" id="ads_enabled" {{ 'checked' if values.ads_enabled }}>
<label for="ads_enabled">{{ _('Ads enabled') }}</label>
</div>
<div class="check">
<input type="checkbox" name="maintenance_mode" id="maintenance_mode" {{ 'checked' if values.maintenance_mode }}>
<label for="maintenance_mode">{{ _('Maintenance mode') }}</label>
</div>
<div class="field">
<label>{{ _('Auto-flag threshold (distinct reports)') }}</label>
<input class="input" type="number" name="flag_threshold" value="{{ values.flag_threshold }}" min="1">
</div>
<div class="field">
<label>{{ _('New-user trust gate (days)') }}</label>
<input class="input" type="number" name="new_user_trust_gate_days" value="{{ values.new_user_trust_gate_days }}" min="0">
</div>
<div class="field">
<label>{{ _('Contact-density flag threshold (signals per message)') }}</label>
<input class="input" type="number" name="contact_density_threshold" value="{{ values.contact_density_threshold }}" min="1">
</div>
<div class="field">
<label>{{ _('Keyword blocklist (one per line)') }}</label>
<textarea class="input" name="keyword_blocklist">{{ values.keyword_blocklist|join('\n') }}</textarea>
</div>
<button class="btn" type="submit">{{ _('Save settings') }}</button>
</form>
</div>
{% endblock %}
+61
View File
@@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block title %}{{ _('Admin · Transactions') }}{% endblock %}
{% block content %}
{% include "admin/_nav.html" %}
<div class="card">
<h2>{{ _('Transactions') }}</h2>
<p class="muted">{{ _('All-time revenue (succeeded): $%(amount)s', amount='%.2f'|format(total_cents / 100)) }}</p>
<form method="get" class="admin-filter-row">
<div class="field">
<label>{{ _('Type') }}</label>
<select class="input" name="type">
<option value="">{{ _('All') }}</option>
<option value="subscription" {{ 'selected' if filters.get('type') == 'subscription' }}>{{ _('Subscription') }}</option>
<option value="boost" {{ 'selected' if filters.get('type') == 'boost' }}>{{ _('Boost') }}</option>
<option value="refund" {{ 'selected' if filters.get('type') == 'refund' }}>{{ _('Refund') }}</option>
</select>
</div>
<div class="field">
<label>{{ _('Status') }}</label>
<select class="input" name="status">
<option value="">{{ _('All') }}</option>
<option value="succeeded" {{ 'selected' if filters.get('status') == 'succeeded' }}>{{ _('Succeeded') }}</option>
<option value="failed" {{ 'selected' if filters.get('status') == 'failed' }}>{{ _('Failed') }}</option>
<option value="pending" {{ 'selected' if filters.get('status') == 'pending' }}>{{ _('Pending') }}</option>
</select>
</div>
<button class="btn ghost" type="submit">{{ _('Filter') }}</button>
{% if filters.get('type') or filters.get('status') %}<a class="btn ghost" href="{{ url_for('admin.transactions') }}">{{ _('Clear') }}</a>{% endif %}
</form>
<table class="list">
<thead><tr>
<th>{{ _('Date') }}</th>
<th>{{ _('User') }}</th>
<th>{{ _('Type') }}</th>
<th>{{ _('Amount') }}</th>
<th>{{ _('Status') }}</th>
<th>{{ _('Stripe ID') }}</th>
</tr></thead>
{% for txn in pagination.items %}
<tr>
<td class="muted small">{{ txn.created_at.strftime('%Y-%m-%d') }}</td>
<td><a href="{{ url_for('admin.user_detail', user_id=txn.user_id) }}">{{ txn.user.display_name }}</a></td>
<td><span class="badge cat">{{ txn.type }}</span></td>
<td>${{ '%.2f'|format(txn.amount_cents / 100) }}</td>
<td><span class="badge {{ 'ok' if txn.status == 'succeeded' else 'warn' }}">{{ txn.status }}</span></td>
<td class="muted small">{{ txn.stripe_object_id or '—' }}</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">{{ _('No transactions.') }}</td></tr>
{% endfor %}
</table>
<div class="pager">
{% if pagination.has_prev %}<a href="{{ url_for('admin.transactions', **merge_query(page=pagination.prev_num)) }}">&larr; {{ _('Prev') }}</a>{% endif %}
<span class="muted">{{ _('Page %(p)s of %(t)s', p=pagination.page, t=pagination.pages) }}</span>
{% if pagination.has_next %}<a href="{{ url_for('admin.transactions', **merge_query(page=pagination.next_num)) }}">{{ _('Next') }} &rarr;</a>{% endif %}
</div>
</div>
{% endblock %}