Add admin page showing every project with the exact prompt submitted
This commit is contained in:
parent
62c4f344c7
commit
dba954ad17
1 changed files with 142 additions and 0 deletions
142
admin.html
Normal file
142
admin.html
Normal 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 · 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>
|
||||
Loading…
Reference in a new issue