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>
80 lines
3.6 KiB
HTML
80 lines
3.6 KiB
HTML
<!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>
|