admin: internal-only superadmin console (users, payments, DB stats)
New independent `admin` service (admin.main:app) on the same image, reusing app.db/app.models read-only. Never runs migrations or the scheduler; issues SELECTs only. - Password-gated (ADMIN_CONSOLE_PASSWORD) with a 12h signed cookie; closed by default when the password is empty. - Bound to 127.0.0.1:8091 (SSH-tunnel access); off the intranet/NPM network. - Pages: overview stats, user list + search, per-user history/payment detail, DB usage (information_schema size + row estimates). - Compose: base `admin` service (+prod DB-host override, test mount); Dockerfile bakes admin/ into runtime + test stages. - Tests: tests/test_admin_console.py (auth, queries, page wiring) — 12 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
8946dee2e0
commit
411094d7b8
20 changed files with 1143 additions and 1 deletions
80
admin/templates/base.html
Normal file
80
admin/templates/base.html
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title>{% block title %}superadmin{% endblock %} · read.markets</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg:#0d1117; --panel:#161b22; --border:#30363d; --fg:#c9d1d9;
|
||||
--muted:#8b949e; --accent:#58a6ff; --good:#3fb950; --warn:#d29922;
|
||||
--bad:#f85149; --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
|
||||
}
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; background:var(--bg); color:var(--fg);
|
||||
font:14px/1.5 var(--mono); }
|
||||
a { color:var(--accent); text-decoration:none; }
|
||||
a:hover { text-decoration:underline; }
|
||||
header { display:flex; align-items:center; gap:1.5rem; padding:.75rem 1.25rem;
|
||||
background:var(--panel); border-bottom:1px solid var(--border); }
|
||||
header .brand { font-weight:700; color:#fff; }
|
||||
header nav { display:flex; gap:1rem; }
|
||||
header .spacer { flex:1; }
|
||||
main { padding:1.25rem; max-width:1200px; margin:0 auto; }
|
||||
h1 { font-size:1.25rem; margin:0 0 1rem; }
|
||||
h2 { font-size:1rem; color:var(--muted); text-transform:uppercase;
|
||||
letter-spacing:.05em; margin:1.5rem 0 .5rem; }
|
||||
.cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr));
|
||||
gap:.75rem; }
|
||||
.card { background:var(--panel); border:1px solid var(--border);
|
||||
border-radius:6px; padding:.75rem 1rem; }
|
||||
.card .n { font-size:1.6rem; font-weight:700; color:#fff; }
|
||||
.card .l { color:var(--muted); font-size:.8rem; }
|
||||
table { width:100%; border-collapse:collapse; margin-top:.5rem; }
|
||||
th,td { text-align:left; padding:.4rem .6rem; border-bottom:1px solid var(--border);
|
||||
white-space:nowrap; }
|
||||
th { color:var(--muted); font-weight:600; font-size:.8rem;
|
||||
text-transform:uppercase; letter-spacing:.04em; }
|
||||
tr:hover td { background:#1c2230; }
|
||||
.pill { display:inline-block; padding:.05rem .5rem; border-radius:999px;
|
||||
font-size:.75rem; border:1px solid var(--border); }
|
||||
.pill.good { color:var(--good); border-color:#238636; }
|
||||
.pill.warn { color:var(--warn); border-color:#9e6a03; }
|
||||
.pill.bad { color:var(--bad); border-color:#a1281f; }
|
||||
.pill.mute { color:var(--muted); }
|
||||
.muted { color:var(--muted); }
|
||||
.bar { height:8px; background:#21262d; border-radius:4px; overflow:hidden;
|
||||
min-width:80px; }
|
||||
.bar > span { display:block; height:100%; background:var(--accent); }
|
||||
form.search { margin:.5rem 0; }
|
||||
input[type=text],input[type=password] {
|
||||
background:#0d1117; border:1px solid var(--border); color:var(--fg);
|
||||
padding:.4rem .6rem; border-radius:6px; font:inherit; }
|
||||
button { background:#238636; color:#fff; border:0; padding:.45rem .9rem;
|
||||
border-radius:6px; font:inherit; cursor:pointer; }
|
||||
button:hover { background:#2ea043; }
|
||||
.kv { display:grid; grid-template-columns:180px 1fr; gap:.25rem 1rem; }
|
||||
.kv dt { color:var(--muted); }
|
||||
.kv dd { margin:0; }
|
||||
.login { max-width:340px; margin:12vh auto; background:var(--panel);
|
||||
border:1px solid var(--border); border-radius:8px; padding:1.5rem; }
|
||||
.err { color:var(--bad); margin:.5rem 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% if show_nav|default(true) %}
|
||||
<header>
|
||||
<span class="brand">read.markets · superadmin</span>
|
||||
<nav>
|
||||
<a href="/">Overview</a>
|
||||
<a href="/users">Users</a>
|
||||
<a href="/db">Database</a>
|
||||
</nav>
|
||||
<span class="spacer"></span>
|
||||
<a href="/logout">Log out</a>
|
||||
</header>
|
||||
{% endif %}
|
||||
<main>{% block body %}{% endblock %}</main>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue