Add admin page showing every project with the exact prompt submitted

This commit is contained in:
gator 2026-09-06 20:02:54 +00:00
parent 62c4f344c7
commit dba954ad17

142
admin.html Normal file
View file

@ -0,0 +1,142 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Admin — PayToLaugh</title>
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="/assets/app.css">
<style>
.tot{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:28px}
.tot div{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 16px}
.tot b{display:block;font-size:22px;letter-spacing:-.01em}
.tot span{font:11px/1.4 var(--mono);color:var(--muted);text-transform:uppercase;letter-spacing:.1em}
.proj{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:20px;margin-bottom:18px}
.proj h3{margin:0 0 4px;font-size:18px}
.proj .who{font:12px/1.5 var(--mono);color:var(--muted);margin-bottom:12px}
.grid2{display:grid;grid-template-columns:170px 1fr;gap:20px;align-items:start}
@media(max-width:700px){.grid2{grid-template-columns:1fr}}
.grid2 video{width:100%;aspect-ratio:9/16;border-radius:10px;border:1px solid var(--line);background:#000}
.kv{font:12px/1.7 var(--mono);color:var(--muted)}
.kv b{color:var(--ink);font-weight:600}
details{border-top:1px solid var(--line);margin-top:14px;padding-top:12px}
summary{cursor:pointer;font:12px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
pre{background:#0f1117;border:1px solid var(--line);border-radius:8px;padding:12px 14px;
overflow-x:auto;font:12px/1.6 var(--mono);color:#cfd6e4;white-space:pre-wrap;word-break:break-word}
.shot{border-left:3px solid var(--line);padding-left:14px;margin:14px 0}
.shot .hd{font:11px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--accent2);margin-bottom:6px}
.warnw{color:#ffb9a7}
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<h1>Admin</h1>
<p>Every project, what it cost, and the exact prompt that was sent for each shot.</p>
</header>
<section>
<div class="msg" id="msg"></div>
<div id="gate" class="card" style="max-width:440px">
<div class="field">
<label for="k">Admin key</label>
<span class="hint">Kept in this browser only. Never in the page source.</span>
<input type="password" id="k" autocomplete="off">
</div>
<button class="btn" id="go">Open</button>
</div>
<div id="out"></div>
</section>
<footer>PayToLaugh &middot; operator view</footer>
</div>
<script src="/assets/app.js"></script>
<script>
var KEY = null;
try { KEY = localStorage.getItem("ptl_admin"); } catch (e) {}
function money(v) { return v == null ? "—" : "$" + Number(v).toFixed(2); }
function when(s) {
if (!s) return "";
var d = new Date(s);
return isNaN(d) ? s : d.toLocaleString();
}
function shotHtml(s) {
var over = s.words > 18;
return '<div class="shot">'
+ '<div class="hd">Shot ' + s.index + (s.continues ? ' · continues' : '')
+ ' · seed: ' + esc(s.seed_used || "unknown") + '</div>'
+ '<p style="margin:0 0 6px">' + esc(s.line || "") + '</p>'
+ '<div class="kv"><span' + (over ? ' class="warnw"' : '') + '>' + s.words + ' words'
+ (over ? ' (over 18)' : '') + '</span> · camera: ' + esc(s.camera || "—") + '</div>'
+ (s.plate_url ? '<div class="kv">opening still: <a href="' + esc(s.plate_url) + '" target="_blank" rel="noopener">view</a></div>' : '')
+ (s.prompt_sent
? '<details><summary>Exact prompt sent</summary><pre>' + esc(s.prompt_sent) + '</pre></details>'
: '<div class="kv">no prompt recorded (filmed before prompts were stored)</div>')
+ '</div>';
}
function projHtml(p) {
var media = p.video_url
? '<video controls playsinline preload="metadata" src="' + esc(p.video_url) + '"></video>'
: '<div class="kv">no video</div>';
return '<div class="proj">'
+ '<div class="grid2"><div>' + media + '</div><div>'
+ '<h3>' + esc(p.title || "(untitled)") + '</h3>'
+ '<div class="who">' + esc(p.user_email || "") + ' · ' + esc(p.pet_name || "") + ' · '
+ esc(p.status || "") + ' · ' + esc(when(p.created_at)) + '</div>'
+ '<div class="kv">'
+ '<b>billed</b> ' + money(p.price_usd) + ' · <b>cost</b> ' + money(p.actual_cost_usd || p.cost_usd)
+ ' · <b>tokens</b> ' + (p.script_tokens || 0)
+ ' · <b>writes</b> ' + (p.script_writes || 0)
+ ' · <b>rewrites</b> ' + (p.revisions || 0) + '<br>'
+ '<b>voice</b> ' + esc(p.pet_voice_formula || "(none set)") + '<br>'
+ '<b>scene</b> ' + esc(p.scene || "(none)") + '<br>'
+ '<b>source</b> <a href="' + esc(p.source_url || "#") + '" target="_blank" rel="noopener nofollow">' + esc((p.source_url || "").slice(0, 70)) + '</a>'
+ (p.film_note ? '<br><span class="warnw"><b>note</b> ' + esc(p.film_note) + '</span>' : '')
+ '</div>'
+ (p.owner_notes ? '<p class="kv" style="margin-top:10px"><b>notes</b> ' + esc(p.owner_notes) + '</p>' : '')
+ '</div></div>'
+ (p.full_text ? '<details><summary>Full piece as written</summary><pre>' + esc(p.full_text) + '</pre></details>' : '')
+ (p.shots && p.shots.length
? '<details open><summary>' + p.shots.length + ' shots</summary>' + p.shots.map(shotHtml).join("") + '</details>'
: '')
+ '</div>';
}
async function load() {
hideMsg("msg");
var r = await api("admin_overview", { admin_key: KEY });
if (!r.ok) {
showMsg("msg", r.error || "Could not load.");
document.getElementById("gate").style.display = "";
return;
}
document.getElementById("gate").style.display = "none";
var t = r.totals || {};
var pct = t.projects ? Math.round((t.accepted_first_draft / t.projects) * 100) : 0;
document.getElementById("out").innerHTML =
'<div class="tot">'
+ '<div><b>' + (t.projects || 0) + '</b><span>projects</span></div>'
+ '<div><b>' + (t.with_video || 0) + '</b><span>filmed</span></div>'
+ '<div><b>' + (t.users || 0) + '</b><span>users</span></div>'
+ '<div><b>' + money(t.billed_usd) + '</b><span>billed</span></div>'
+ '<div><b>' + money(t.spent_usd) + '</b><span>our cost</span></div>'
+ '<div><b>' + (t.tokens || 0).toLocaleString() + '</b><span>script tokens</span></div>'
+ '<div><b>' + (t.rewrites || 0) + '</b><span>rewrites asked</span></div>'
+ '<div><b>' + pct + '%</b><span>took first draft</span></div>'
+ '</div>'
+ (r.projects || []).map(projHtml).join("");
}
document.getElementById("go").onclick = function () {
KEY = document.getElementById("k").value.trim();
if (!KEY) { showMsg("msg", "Enter the admin key."); return; }
try { localStorage.setItem("ptl_admin", KEY); } catch (e) {}
load();
};
if (KEY) load();
</script>
</body>
</html>