010e8d7ffc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
493 lines
40 KiB
HTML
493 lines
40 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>Admin | LinkSpin</title>
|
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
|
<link rel="icon" type="image/png" href="/logo-icon.png">
|
|
<link rel="stylesheet" href="/assets/site.css?v=20260910h">
|
|
<style>
|
|
.adm-table{width:100%;table-layout:auto}
|
|
.adm-table th{cursor:pointer;user-select:none;white-space:nowrap}
|
|
#trfSources th,#trfAngles th{white-space:normal;text-align:center;line-height:1.2;vertical-align:bottom}
|
|
#trfSources td:not(:first-child),#trfAngles td:not(:first-child){text-align:center;font-variant-numeric:tabular-nums}
|
|
.adm-table th.sort-asc::after{content:' \25B2';font-size:9px;color:var(--mint)}
|
|
.adm-table th.sort-desc::after{content:' \25BC';font-size:9px;color:var(--mint)}
|
|
.adm-table.kv th{cursor:default;color:var(--muted);font-weight:500}
|
|
.adm-table.kv th.sort-asc::after,.adm-table.kv th.sort-desc::after{content:''}
|
|
.adm-table td.act,.adm-table th:last-child{width:1%;white-space:nowrap} /* the action column keeps its full width; text columns give way */
|
|
.adm-table td:first-child{overflow-wrap:anywhere;word-break:break-word}
|
|
.adm-table th,.adm-table td{padding:8px 10px;text-align:left;vertical-align:top;border-bottom:1px solid var(--line);font-size:13.5px}
|
|
.adm-table td.when{white-space:nowrap}
|
|
.adm-table th{color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.08em}
|
|
.adm-table .act{white-space:nowrap}
|
|
.adm-table .act button{margin-right:6px}
|
|
.st{display:inline-block;border-radius:999px;padding:2px 9px;font-size:11.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase}
|
|
.st.active{background:rgba(67,232,195,.14);color:var(--mint)}
|
|
.st.paused{background:rgba(255,178,56,.14);color:var(--amber)}
|
|
.st.out{background:rgba(255,255,255,.08);color:var(--muted)}
|
|
.house-tag{display:inline-block;border:1px solid rgba(157,125,255,.5);color:var(--violet);border-radius:6px;padding:1px 6px;font-size:10.5px;font-weight:700;margin-left:6px;vertical-align:middle}
|
|
.adm-form p{margin:0 0 10px}
|
|
.adm-form label.small{display:block;margin-bottom:4px}
|
|
.trunc{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:bottom}
|
|
textarea.json{font-family:var(--mono);font-size:12.5px;width:100%;min-height:260px}
|
|
.ph-chips{display:inline-flex;gap:6px;flex-wrap:wrap;margin-left:6px;vertical-align:middle}
|
|
.ph-chips .chip-t{padding:2px 10px;font-size:11.5px;font-family:var(--mono);text-transform:none;letter-spacing:0;cursor:pointer;background:transparent}
|
|
.drip-steps{display:grid;gap:12px}
|
|
.drip-step{border:1px solid var(--line);border-radius:12px;padding:14px 16px;background:rgba(4,8,7,.4)}
|
|
.drip-step .ds-head{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:8px}
|
|
.drip-step .ds-n{font-family:var(--mono);font-size:12px;color:var(--mint);letter-spacing:.08em}
|
|
.drip-step .ds-when{display:flex;align-items:center;gap:6px;font-size:13px;color:var(--muted)}
|
|
.drip-step .ds-when input{width:80px;text-align:right}
|
|
.drip-step .ds-when b{color:var(--ink)}
|
|
.drip-step .ds-tools{margin-left:auto;display:flex;gap:6px;flex-wrap:wrap}
|
|
.drip-step .ds-tools .btn{padding:6px 12px;font-size:12px}
|
|
.drip-step input.ds-subject{width:100%;margin-bottom:8px;font-weight:700}
|
|
.drip-step textarea.ds-body{width:100%;min-height:200px;font-size:13.5px;line-height:1.5}
|
|
.rates-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px 22px}
|
|
.rates-form .rf{display:flex;flex-direction:column;gap:4px}
|
|
.rates-form .rf label{font-size:13px;font-weight:700}
|
|
.rates-form .rf .hint{font-size:11.5px;color:var(--muted)}
|
|
.rates-form .rf input[type=number]{max-width:160px}
|
|
.rates-form .rf.wide{grid-column:1/-1}
|
|
.rates-form .sub-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:8px}
|
|
.rates-form .sub-grid label{font-size:12px;font-weight:600;color:var(--muted);display:flex;flex-direction:column;gap:3px}
|
|
.rates-form table.tiers{width:auto;font-size:13px}
|
|
.rates-form table.tiers td,.rates-form table.tiers th{padding:4px 8px;border:0}
|
|
.rates-form table.tiers input{width:90px}
|
|
.kv-form{display:grid;gap:8px}
|
|
.kv-row{display:grid;grid-template-columns:220px 1fr auto;gap:10px;align-items:center}
|
|
.kv-row .k{font-family:var(--mono);font-size:12.5px;overflow-wrap:anywhere}
|
|
.kv-row input[type=checkbox]{width:auto;justify-self:start}
|
|
@media(max-width:700px){.kv-row{grid-template-columns:1fr}}
|
|
</style>
|
|
</head>
|
|
<body class="bo-body">
|
|
|
|
<!-- signed-out: admin sign-in -->
|
|
<div id="authArea">
|
|
<div class="wrap" style="max-width:520px">
|
|
<section class="hero" style="padding:64px 0 10px;text-align:left">
|
|
<p class="eyebrow">LinkSpin</p>
|
|
<h1 style="font-size:34px">Admin <em>sign in</em></h1>
|
|
<p class="lead" style="font-size:16px">Only the admin address can sign in here. A one-time code goes to that inbox.</p>
|
|
</section>
|
|
<div class="card">
|
|
<p><input id="adEmail" type="email" placeholder="Admin email" autocomplete="email" style="width:100%"></p>
|
|
<p id="adCodeRow" hidden><input id="adCode" inputmode="numeric" placeholder="6-digit code from your email" style="width:100%"></p>
|
|
<p id="adErr" class="small" style="color:var(--bad)" hidden></p>
|
|
<p style="margin:0;display:flex;gap:10px">
|
|
<button class="btn" id="adSend" type="button">Send code</button>
|
|
<button class="btn" id="adVerify" type="button" hidden>Sign in</button>
|
|
</p>
|
|
<p class="small muted" style="margin:14px 0 0"><a href="/my">Back to the member area</a></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- signed-in: admin shell -->
|
|
<div id="adminArea" class="bo" hidden>
|
|
<aside class="bo-side" id="boSide">
|
|
<a class="logo" href="/"><img src="/logo.png" alt="LinkSpin" style="height:32px;display:block"></a>
|
|
<span class="byline">Brought to you by the <b>Crypto Team Build Network</b></span>
|
|
<nav class="bo-menu" aria-label="Admin menu">
|
|
<button data-pane="overview" class="on" type="button"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="8" height="8" rx="2"/><rect x="13" y="3" width="8" height="8" rx="2"/><rect x="3" y="13" width="8" height="8" rx="2"/><rect x="13" y="13" width="8" height="8" rx="2"/></svg>Overview</button>
|
|
<button data-pane="house" type="button"><svg viewBox="0 0 24 24"><path d="M3 11l14-5v12L3 13v-2z"/><path d="M17 8a4 4 0 0 1 0 8M7 13v5a2 2 0 0 0 4 0v-3"/></svg>House ads</button>
|
|
<button data-pane="campaigns" type="button"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h10"/></svg>All campaigns</button>
|
|
<button data-pane="members" type="button"><svg viewBox="0 0 24 24"><circle cx="12" cy="7" r="3.2"/><circle cx="5" cy="17" r="2.6"/><circle cx="19" cy="17" r="2.6"/><path d="M12 10v3M12 13l-5 2M12 13l5 2"/></svg>Members</button>
|
|
<button data-pane="reports" type="button"><svg viewBox="0 0 24 24"><path d="M12 3l9 16H3z"/><path d="M12 10v4M12 17v.5"/></svg>Reports<span class="pill" id="repBadge" hidden></span></button>
|
|
<button data-pane="traffic" type="button"><svg viewBox="0 0 24 24"><path d="M3 17l6-6 4 4 8-8"/><path d="M14 7h7v7"/></svg>Traffic</button>
|
|
<button data-pane="blog" type="button"><svg viewBox="0 0 24 24"><path d="M4 4h12l4 4v12H4z"/><path d="M8 12h8M8 16h8M8 8h4"/></svg>Blog</button>
|
|
<button data-pane="releases" type="button"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h10M4 18h7"/><circle cx="18" cy="17" r="3"/></svg>Releases</button>
|
|
<button data-pane="pnl" type="button"><svg viewBox="0 0 24 24"><path d="M4 19V5M4 19h16"/><path d="M8 15l3-4 3 2 5-6"/></svg>P&L</button>
|
|
<button data-pane="settings" type="button"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>Settings</button>
|
|
</nav>
|
|
<div class="bo-links">
|
|
<span class="bo-cap">Site</span>
|
|
<a href="/my">Member area</a>
|
|
<a href="/ledger" target="_blank" rel="noopener">Live ledger</a>
|
|
<a href="/" target="_blank" rel="noopener">Home page</a>
|
|
</div>
|
|
<div class="bo-foot">
|
|
<span id="adWho" class="small muted">…</span>
|
|
<a href="#" id="adLogout" class="small">Log out</a>
|
|
</div>
|
|
</aside>
|
|
|
|
<div class="bo-main">
|
|
<header class="bo-top">
|
|
<button id="boBurger" aria-label="Menu" type="button">☰</button>
|
|
<h2 id="boTitle" style="margin:0">Overview</h2>
|
|
<span class="small muted" id="chainLine"></span>
|
|
</header>
|
|
<main class="bo-content">
|
|
|
|
<div class="pane" id="pane-overview">
|
|
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));margin-top:0">
|
|
<div class="statx"><div><div class="nv" id="ovAccounts">0</div><div class="lb">Accounts</div></div></div>
|
|
<div class="statx c-cyan"><div><div class="nv" id="ovMembers">0</div><div class="lb">On-chain members</div></div></div>
|
|
<div class="statx c-violet"><div><div class="nv" id="ovActive">0</div><div class="lb">Active campaigns</div><span class="chip flat" id="ovCampSub"></span></div></div>
|
|
<div class="statx c-amber"><div><div class="nv" id="ovReports">0</div><div class="lb">Open reports</div><span class="chip flat" id="ovBurnSub"></span></div></div>
|
|
<div class="statx"><div><div class="nv" id="ovDrips">0</div><div class="lb">Follow-ups in flight</div><span class="chip flat" id="ovDripSub"></span></div></div>
|
|
</div>
|
|
<div class="grid c2" style="margin-top:16px">
|
|
<div class="card">
|
|
<div class="card-head"><h3>Campaigns by format</h3><span class="sub">all owners</span></div>
|
|
<div id="ovByType" class="small muted">…</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Chain</h3><span class="sub">live config</span></div>
|
|
<div id="ovChain" class="small">…</div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Quick actions</h3></div>
|
|
<p style="display:flex;gap:10px;flex-wrap:wrap;margin:0">
|
|
<button class="btn small" type="button" data-goto="house">Place a house ad</button>
|
|
<button class="btn small sec" type="button" data-goto="campaigns">Review campaigns</button>
|
|
<button class="btn small sec" type="button" data-goto="members">Members</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pane" id="pane-house" hidden>
|
|
<div class="card adm-form">
|
|
<div class="card-head"><h3>New house ad</h3><span class="sub">free: nothing is charged, the budget is only a delivery cap</span></div>
|
|
<div class="grid c3">
|
|
<p><select id="hType" style="width:100%">
|
|
<option value="banner">Banner (per impression)</option>
|
|
<option value="text">Text ad (per impression)</option>
|
|
<option value="login">Login ad (full-screen after sign-in)</option>
|
|
<option value="solo">Solo ad (inbox delivery)</option>
|
|
<option value="video">Video ad (watch to earn)</option>
|
|
<option value="featured">Featured link (rotation)</option>
|
|
<option value="visits">Verified visits</option>
|
|
</select></p>
|
|
<p><input id="hName" placeholder="Campaign name" style="width:100%"></p>
|
|
<p><input id="hBudget" type="number" min="10" placeholder="Delivery cap in credits (default 100,000)" style="width:100%"></p>
|
|
</div>
|
|
<p><input id="hTarget" placeholder="Target URL (https://…)" autocomplete="off" style="width:100%"></p>
|
|
<p class="small muted" style="margin-top:-4px">Banner and text targets are framed in the ad viewer, so they must allow framing (checked on submit).</p>
|
|
<div id="hBannerRow">
|
|
<p><input id="hImage" placeholder="Image URL (https://… or /uploads/…)" autocomplete="off" style="width:100%"></p>
|
|
<p><input type="file" id="hImageFile" accept="image/png,image/jpeg,image/webp,image/gif" hidden>
|
|
<button type="button" class="btn small sec" id="hImageUploadBtn">Upload image</button>
|
|
<span id="hImageInfo" class="small muted"></span></p>
|
|
<p><select id="hSize" style="width:100%"></select></p>
|
|
</div>
|
|
<div id="hTextRow" hidden>
|
|
<p><input id="hTitle" maxlength="60" placeholder="Headline (max 60)" style="width:100%"></p>
|
|
<p><input id="hBody" maxlength="140" placeholder="Ad text (max 140)" style="width:100%"></p>
|
|
</div>
|
|
<div id="hSoloRow" hidden>
|
|
<p><input id="hSoloTitle" maxlength="80" placeholder="Subject line (max 80)" style="width:100%"></p>
|
|
<p><textarea id="hSoloBody" rows="8" placeholder="Message body. Simple HTML is fine (bold, lists, links, images)." style="width:100%"></textarea></p>
|
|
<p><input id="hSoloCta" maxlength="30" placeholder="Call-to-action label (default: Learn more)" style="width:100%"></p>
|
|
<p class="small muted" id="hSoloHint"></p>
|
|
</div>
|
|
<div id="hVideoRow" hidden>
|
|
<p><input id="hVideoUrl" placeholder="Video URL (direct https link ending .mp4 or .webm, or /uploads/…)" autocomplete="off" style="width:100%"></p>
|
|
<p><input type="file" id="hVideoFile" accept="video/mp4,video/webm" hidden>
|
|
<button type="button" class="btn small sec" id="hVideoUploadBtn">Upload video</button>
|
|
<span id="hVideoInfo" class="small muted"></span></p>
|
|
<p><input id="hVideoTitle" maxlength="80" placeholder="Video title (optional)" style="width:100%"></p>
|
|
<p><select id="hWatchSecs" style="width:100%"></select></p>
|
|
<p><input id="hVideoCta" maxlength="30" placeholder="Call-to-action label (default: Learn more)" style="width:100%"></p>
|
|
</div>
|
|
<div id="hFeatRow" hidden>
|
|
<p><input id="hFeatTitle" maxlength="70" placeholder="Headline for the featured link" style="width:100%"></p>
|
|
<div class="grid c2">
|
|
<p><select id="hFeatDays" style="width:100%"></select></p>
|
|
<p><input id="hFeatStart" type="number" min="0" value="0" placeholder="Start in N days (0 = today)" style="width:100%"></p>
|
|
</div>
|
|
<p class="small muted" id="hFeatHint"></p>
|
|
</div>
|
|
<div id="hVisitsRow" hidden>
|
|
<p><input id="hVisitTitle" maxlength="80" placeholder="Headline (what members see before visiting)" style="width:100%"></p>
|
|
<p><input id="hVisitCount" type="number" min="20" placeholder="How many verified visits?" style="width:100%"></p>
|
|
<p class="small muted" id="hVisitHint"></p>
|
|
</div>
|
|
<p id="hErr" class="small" style="color:var(--bad)" hidden></p>
|
|
<button class="btn" id="hCreate" type="button">Place house ad</button>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>House ads</h3><span class="sub" id="houseSub">running free</span></div>
|
|
<div class="tablewrap"><table class="adm-table" id="houseTable"></table></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Wall fallback ads</h3><span class="sub" id="wallAdsSub">shown on member walls in positions they have not earned or filled, when no upline banner exists</span></div>
|
|
<p class="small muted">A member's wall has three positions. Position 1 is theirs. Positions 2 and 3 show an upline's banner until the member earns them (2 and 5 qualifying buyers); when there is no upline banner, one of these ads shows instead. They rotate in order across walls. Leave the list empty to fall back to a plain LinkSpin card.</p>
|
|
<div id="wallAdsList" class="drip-steps"></div>
|
|
<p style="display:flex;gap:10px;flex-wrap:wrap;margin:12px 0 0">
|
|
<button class="btn small" id="wallAdsSave" type="button">Save wall ads</button>
|
|
<button class="btn small sec" id="wallAdsAdd" type="button">+ Add a wall ad</button>
|
|
</p>
|
|
<p id="wallAdsErr" class="small" style="color:var(--bad)" hidden></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pane" id="pane-campaigns" hidden>
|
|
<div class="card">
|
|
<div class="card-head"><h3>All campaigns</h3><span class="sub" id="campSub">newest first</span></div>
|
|
<p style="margin:0 0 10px"><input id="campFilter" placeholder="Filter by owner, name, type or status" style="width:100%"></p>
|
|
<div class="tablewrap"><table class="adm-table" id="campTable"></table></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pane" id="pane-members" hidden>
|
|
<div class="card" id="memSearchCard">
|
|
<div class="card-head"><h3>Find a member</h3><span class="sub">matches as you type; Enter opens the first match</span></div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap"><input id="memSearch" autocomplete="off" placeholder="Start typing a name, username, email, member # or wallet…" style="flex:1;min-width:240px"><button type="button" class="btn small" id="memOpen">Open</button></div>
|
|
<div id="memHits" hidden style="margin-top:6px;border:1px solid var(--line);border-radius:10px;overflow:hidden"></div>
|
|
<p class="small" id="memSearchMsg" hidden style="margin:8px 0 0"></p>
|
|
</div>
|
|
<div class="card" id="memCard" hidden>
|
|
<div class="card-head"><h3 id="mcName">Member</h3><span class="sub" id="mcSub"></span></div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;margin:0 0 12px">
|
|
<button type="button" class="btn small sec" id="mcBack">← Back to list</button>
|
|
<button type="button" class="btn small sec" data-mcact="username">Set username</button>
|
|
<button type="button" class="btn small sec" data-mcact="sponsor">Set sponsor</button>
|
|
<button type="button" class="btn small sec" data-mcact="wallet">Swap main wallet</button>
|
|
<button type="button" class="btn small sec" data-mcact="credits">Grant credits</button>
|
|
<a class="btn small sec" id="mcWall" href="#" target="_blank" rel="noopener">Public wall</a>
|
|
<button type="button" class="btn small sec" data-mcact="delete" style="margin-left:auto">Delete account</button>
|
|
</div>
|
|
<div id="mcBody"></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Holding tank</h3><span class="sub" id="tankAdmSub">free members with no sponsor, and who adopted whom</span></div>
|
|
<div class="tablewrap"><table class="adm-table" id="tankWait"></table></div>
|
|
<p class="small muted" style="margin:12px 0 6px">Adoptions (newest first)</p>
|
|
<div class="tablewrap"><table class="adm-table" id="tankAdopt"></table></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Members</h3><span class="sub" id="memSub">newest first</span></div>
|
|
<p style="margin:0 0 10px"><input id="memFilter" placeholder="Filter by email, username, member # or sponsor" style="width:100%"></p>
|
|
<p class="small muted">Sponsor = the token the account joined under (username, share code, or member #). Editing it re-points free referrals and future purchases. On-chain sponsorship is permanent once activated.</p>
|
|
<div class="tablewrap"><table class="adm-table" id="memTable"></table></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pane" id="pane-reports" hidden>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Counters audit</h3><span class="sub" id="audSub">views vs delivery logs, charges vs shows</span></div>
|
|
<p class="small muted" style="margin:0 0 8px">Every format's recorded views are reconciled against the log that proves delivery, login days charged against days shown, and featured bookings checked for views. Runs daily and alerts you on Telegram; run it any time here.</p>
|
|
<div style="margin:0 0 8px"><button type="button" class="btn small sec" id="audRun">Run now</button></div>
|
|
<div class="tablewrap"><table class="adm-table" id="audTable"></table></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Ad reports</h3><span class="sub">members flagging ads</span></div>
|
|
<div class="tablewrap"><table class="adm-table" id="repTable"></table></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Pending burns</h3><span class="sub">on-chain credit burns waiting to be executed</span></div>
|
|
<div class="tablewrap"><table class="adm-table" id="burnTable"></table></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pane" id="pane-traffic" hidden>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Where visitors come from</h3><span class="sub" id="trfSub"></span></div>
|
|
<div class="chips" id="trfRange" style="margin:0 0 10px"><button type="button" class="chip-t" data-days="7">7 days</button><button type="button" class="chip-t on" data-days="30">30 days</button><button type="button" class="chip-t" data-days="90">90 days</button><button type="button" class="chip-t" data-days="365">Year</button></div>
|
|
<p class="muted small" style="margin:0 0 10px">Page views are public-page loads by referring domain (crawlers skipped; our own pages and no referrer count as direct). Join-page views are invite-link opens. Signups, registered and $20+ buyers are accounts whose first-touch source was that domain; legacy arrivals show as legacy:brand:domain.</p>
|
|
<p style="margin:0 0 6px;display:flex;gap:8px;align-items:center"><input class="tfilter small" data-for="trfSources" placeholder="Filter sources" style="max-width:260px"><span class="tfilter-count muted small"></span></p><div class="tablewrap"><table class="adm-table" id="trfSources"></table></div>
|
|
</div>
|
|
<div class="grid c2">
|
|
<div class="card"><div class="card-head"><h3>Landing pages</h3><span class="sub">page views by page</span></div><p style="margin:0 0 6px;display:flex;gap:8px;align-items:center"><input class="tfilter small" data-for="trfPaths" placeholder="Filter pages" style="max-width:260px"><span class="tfilter-count muted small"></span></p><div class="tablewrap"><table class="adm-table" id="trfPaths"></table></div></div>
|
|
<div class="card"><div class="card-head"><h3>Angles</h3><span class="sub">join-page hook copy</span></div><p style="margin:0 0 6px;display:flex;gap:8px;align-items:center"><input class="tfilter small" data-for="trfAngles" placeholder="Filter angles" style="max-width:260px"><span class="tfilter-count muted small"></span></p><div class="tablewrap"><table class="adm-table" id="trfAngles"></table></div></div>
|
|
</div>
|
|
<div class="card"><div class="card-head"><h3>By day</h3><span class="sub">page views, signups</span></div><p style="margin:0 0 6px;display:flex;gap:8px;align-items:center"><input class="tfilter small" data-for="trfDaily" placeholder="Filter days" style="max-width:260px"><span class="tfilter-count muted small"></span></p><div class="tablewrap"><table class="adm-table" id="trfDaily"></table></div></div>
|
|
<div class="card" id="promoAdmin">
|
|
<div class="card-head"><h3>Partner promo codes</h3><span class="sub">free ad credits for members who redeem a partner's code</span></div>
|
|
<p class="muted small" style="margin:0 0 10px">Give a site owner a code. Their members redeem it on a join link (<code>linkspin-test.saasy.top/join/martbost?promo=CODE</code>) or in the "Have a promo code?" box on the Overview. One use per account; uses and the last redemptions are listed below.</p>
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;align-items:end">
|
|
<label class="small">Code<input id="pcCode" type="text" placeholder="TRAFFICWAVE" style="text-transform:uppercase"></label>
|
|
<label class="small">Credits<input id="pcCredits" type="number" min="1" placeholder="250"></label>
|
|
<label class="small">Partner<input id="pcPartner" type="text" placeholder="site or owner"></label>
|
|
<label class="small">Max uses (0 = unlimited)<input id="pcMax" type="number" min="0" value="0"></label>
|
|
<label class="small">Expires (optional)<input id="pcExpires" type="date"></label>
|
|
<button type="button" class="btn small" id="pcSave">Save code</button>
|
|
</div>
|
|
<p class="small" id="pcMsg" hidden style="margin:8px 0 0"></p>
|
|
<div class="tablewrap" style="margin-top:12px"><table class="adm-table" id="pcTable"></table></div>
|
|
<div class="tablewrap" style="margin-top:12px"><table class="adm-table" id="pcRecent"></table></div>
|
|
</div>
|
|
</div>
|
|
<div class="pane" id="pane-blog" hidden>
|
|
<div class="card" id="blogList">
|
|
<div class="card-head"><h3>Articles</h3><span class="sub" id="blSub"></span></div>
|
|
<p class="muted small" style="margin:0 0 10px">Coaching and teaching stories, published at <a href="/blog" target="_blank" rel="noopener">linkspin-test.saasy.top/blog</a>. Every published post gets its own page with a title tag, description, canonical link, social preview card, structured data, and a spot in the sitemap and RSS feed. Drafts are visible only to you (open one from its row to preview the real page).</p>
|
|
<p class="small muted" id="blSyndNote" hidden style="margin:0 0 10px"></p>
|
|
<p><button type="button" class="btn small" id="blNew">New article</button></p>
|
|
<div class="tablewrap"><table class="adm-table" id="blTable"></table></div>
|
|
</div>
|
|
<div class="card" id="blogEditor" hidden>
|
|
<div class="card-head"><h3 id="blEdTitle">New article</h3><span class="sub" id="blEdSub"></span></div>
|
|
<div style="display:grid;gap:10px">
|
|
<label class="small">Title (the headline and the browser title; under 60 characters shows whole in search results) <span id="blTitleCount" class="muted"></span><input id="blTitle" type="text" maxlength="140" placeholder="The thimble and the bucket"></label>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
|
<label class="small">URL slug (letters, numbers, dashes)<input id="blSlug" type="text" maxlength="80" placeholder="auto from the title"></label>
|
|
<label class="small">Tags (comma separated; the first one shows as the category)<input id="blTags" type="text" maxlength="200" placeholder="mindset, team building"></label>
|
|
</div>
|
|
<label class="small">Excerpt (the meta description and the card text; 120 to 160 characters is ideal) <span id="blExcCount" class="muted"></span><textarea id="blExcerpt" maxlength="300" rows="2" style="width:100%"></textarea></label>
|
|
<div style="display:grid;grid-template-columns:1fr auto;gap:8px;align-items:end">
|
|
<label class="small">Cover image (1200x630 works best; it becomes the social preview card)<input id="blCover" type="text" placeholder="/uploads/... or https://..."></label>
|
|
<span><button type="button" class="btn small sec" id="blCoverBtn">Upload</button><input type="file" id="blCoverFile" accept="image/png,image/jpeg,image/webp" hidden></span>
|
|
</div>
|
|
<p class="small muted" id="blCoverInfo" style="margin:-4px 0 0"></p>
|
|
<div>
|
|
<div class="ed-bar" aria-label="Formatting">
|
|
<button type="button" data-bl="bold" title="Bold"><b>B</b></button>
|
|
<button type="button" data-bl="italic" title="Italic"><i>I</i></button>
|
|
<button type="button" data-blblock="h2" title="Section heading">H2</button>
|
|
<button type="button" data-blblock="h3" title="Sub heading">H3</button>
|
|
<button type="button" data-blblock="p" title="Paragraph">P</button>
|
|
<button type="button" data-blblock="blockquote" title="Quote">Quote</button>
|
|
<button type="button" data-bl="insertUnorderedList" title="Bullet list">• List</button>
|
|
<button type="button" data-bl="insertOrderedList" title="Numbered list">1. List</button>
|
|
<button type="button" id="blLinkBtn" title="Insert link">🔗 Link</button>
|
|
<button type="button" id="blImgBtn" title="Insert image">🖼 Image</button><input type="file" id="blImgFile" accept="image/png,image/jpeg,image/webp,image/gif" hidden>
|
|
<button type="button" id="blHtmlBtn" title="Edit the HTML directly"></></button>
|
|
</div>
|
|
<div id="blBody" class="ed-body" contenteditable="true" data-ph="Write the article. Headings break it up; short paragraphs read better on phones." style="min-height:360px;line-height:1.6"></div>
|
|
<textarea id="blHtml" class="ed-body" hidden style="min-height:360px;font-family:var(--mono);font-size:13px"></textarea>
|
|
<p class="small muted" style="margin:6px 0 0"><span id="blWords">0 words</span> · 600 or more gives a page something to rank on; one idea per section.</p>
|
|
</div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
|
|
<button type="button" class="btn small" id="blSaveDraft">Save draft</button>
|
|
<button type="button" class="btn small" id="blPublish">Publish</button>
|
|
<button type="button" class="btn small sec" id="blUnpublish" hidden>Back to draft</button>
|
|
<a class="btn small sec" id="blPreview" href="#" target="_blank" rel="noopener" hidden>Open page</a>
|
|
<button type="button" class="btn small sec" id="blClose">Close</button>
|
|
<button type="button" class="btn small sec" id="blDelete" hidden style="margin-left:auto">Delete</button>
|
|
</div>
|
|
<p class="small" id="blMsg" hidden></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="pane" id="pane-releases" hidden>
|
|
<div class="grid c2">
|
|
<div class="card">
|
|
<div class="card-head"><h3>Release note</h3><span class="sub">what shipped; shows on /whats-new and every Overview</span></div>
|
|
<div style="display:grid;gap:8px">
|
|
<input type="hidden" id="rnId">
|
|
<label class="small">Title<input id="rnTitle" type="text" maxlength="120" placeholder="Achievement badges post to Telegram"></label>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
<label class="small">Date<input id="rnDate" type="date"></label>
|
|
<label class="small">Tags (new, improved, fixed)<input id="rnTags" type="text" placeholder="new, improved"></label>
|
|
</div>
|
|
<label class="small">Details (plain text; start a line with "- " for a bullet)<textarea id="rnBody" rows="6" style="width:100%"></textarea></label>
|
|
<div style="display:flex;gap:8px"><button type="button" class="btn small" id="rnSave">Save note</button><button type="button" class="btn small sec" id="rnClear">Clear</button></div>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Roadmap item</h3><span class="sub">what is coming; "done" hides it from the public list</span></div>
|
|
<div style="display:grid;gap:8px">
|
|
<input type="hidden" id="rmId">
|
|
<label class="small">Title<input id="rmTitle" type="text" maxlength="120" placeholder="Second daily ad set"></label>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">
|
|
<label class="small">Status<select id="rmStatus"><option value="planned">planned</option><option value="building">building</option><option value="done">done</option></select></label>
|
|
<label class="small">ETA (free text)<input id="rmEta" type="text" placeholder="this week"></label>
|
|
<label class="small">Order<input id="rmOrder" type="number" min="1" placeholder="1"></label>
|
|
</div>
|
|
<label class="small">One-line note<input id="rmNote" type="text" maxlength="600" placeholder="why it matters"></label>
|
|
<div style="display:flex;gap:8px"><button type="button" class="btn small" id="rmSave">Save item</button><button type="button" class="btn small sec" id="rmClear">Clear</button></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="grid c2">
|
|
<div class="card"><div class="card-head"><h3>Notes</h3><span class="sub" id="rnSub"></span></div><div class="tablewrap"><table class="adm-table" id="rnTable"></table></div></div>
|
|
<div class="card"><div class="card-head"><h3>Roadmap</h3><span class="sub" id="rmSub"></span></div><div class="tablewrap"><table class="adm-table" id="rmTable"></table></div></div>
|
|
</div>
|
|
<div class="card" id="updCard">
|
|
<div class="card-head"><h3>Email an update to members</h3><span class="sub" id="updSub"></span></div>
|
|
<p class="small muted" style="margin:0 0 10px">Pick the notes to include, add a line or two in your own words, preview, send yourself a test, then send. Plain text from no-reply@linkspin-test.saasy.top, one member at a time. The default audience is members who ticked the newsletter box at sign-up (the Sendy list is the record); unsubscribes are skipped. Nothing goes out until you press Send.</p>
|
|
<div class="grid c2">
|
|
<div style="display:grid;gap:8px">
|
|
<label class="small">Subject<input id="updSubject" type="text" maxlength="150" placeholder="What is new on LinkSpin this week"></label>
|
|
<label class="small">Intro (optional, plain text)<textarea id="updIntro" rows="4" style="width:100%" placeholder="Quick one. Three things shipped since my last note, and the third one is the one to try today."></textarea></label>
|
|
<label class="small">Closing (after the notes, e.g. your sign-off)<textarea id="updClosing" rows="2" style="width:100%" placeholder="See you Monday. Marty"></textarea></label>
|
|
<label class="small">Audience<select id="updAudience"></select></label>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap"><button type="button" class="btn small sec" id="updSaveDraft">Save draft</button><button type="button" class="btn small sec" id="updPreview">Preview</button><button type="button" class="btn small sec" id="updTest">Send test to me</button><button type="button" class="btn small" id="updSend">Send</button></div>
|
|
<pre id="updPre" class="small" hidden style="white-space:pre-wrap;background:rgba(255,255,255,.04);border:1px solid var(--line);border-radius:10px;padding:10px;max-height:320px;overflow:auto"></pre>
|
|
</div>
|
|
<div>
|
|
<div class="small muted" style="margin:0 0 6px">Release notes to include (newest first; notes since your last send are pre-ticked)</div>
|
|
<div id="updNotes" style="display:grid;gap:4px;max-height:260px;overflow:auto"></div>
|
|
<div class="small muted" style="margin:12px 0 6px">Recent sends</div>
|
|
<div class="tablewrap"><table class="adm-table" id="updLog"></table></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="small muted">Public page: <a href="/whats-new" target="_blank" rel="noopener">linkspin-test.saasy.top/whats-new</a></p>
|
|
</div>
|
|
<div class="pane" id="pane-pnl" hidden>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Profit and loss</h3><span class="sub">read from the chain index; periods are by block (about 43,200 Polygon blocks a day)</span></div>
|
|
<div class="chips" id="pnlPeriods"><button type="button" class="chip-t" data-days="7">7 days</button><button type="button" class="chip-t on" data-days="30">30 days</button><button type="button" class="chip-t" data-days="90">90 days</button><button type="button" class="chip-t" data-days="0">All time</button></div>
|
|
<div class="grid" id="pnlTiles" style="grid-template-columns:repeat(auto-fit,minmax(200px,1fr));margin-top:12px"></div>
|
|
</div>
|
|
<div class="grid c2">
|
|
<div class="card">
|
|
<div class="card-head"><h3>Where the money went</h3><span class="sub">per period</span></div>
|
|
<div class="tablewrap"><table class="adm-table" id="pnlSplit"></table></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Wallets</h3><span class="sub">live balances</span></div>
|
|
<div class="tablewrap"><table class="adm-table" id="pnlWallets"></table></div>
|
|
<p class="small muted" style="margin:10px 0 0">Fixed monthly cost (USD) for the net line: <input type="number" id="pnlFixed" min="0" step="1" style="width:110px"> <button class="btn small sec" type="button" id="pnlFixedSave">Save</button></p>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Automatic credit burner</h3><span class="sub">settles pending campaign spend on-chain from the engine wallet</span></div>
|
|
<p class="small" id="burnerLine">…</p>
|
|
<p><button class="btn small sec" type="button" id="burnerRun">Run now</button></p>
|
|
</div>
|
|
</div>
|
|
<div class="pane" id="pane-settings" hidden>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Follow-up emails</h3><span class="sub" id="dripSub">sent to every new free member, hours after sign-up</span></div>
|
|
<p class="small muted">One card per email. Placeholders fill in per reader: click a chip to drop it where your cursor is.
|
|
<span class="ph-chips" id="phChips">
|
|
<button type="button" class="chip-t" data-ph="{{link}}" title="the reader's own invite link">{{link}}</button>
|
|
<button type="button" class="chip-t" data-ph="{{sponsor}}" title="their sponsor's name">{{sponsor}}</button>
|
|
<button type="button" class="chip-t" data-ph="{{site}}" title="https://linkspin-test.saasy.top">{{site}}</button>
|
|
<button type="button" class="chip-t" data-ph="{{paid:words if they bought|words if they have not}}" title="two versions: bought / not yet">{{paid:…|…}}</button>
|
|
<button type="button" class="chip-t" data-ph="{{footer}}" title="unsubscribe line + disclaimer (keep it at the end)">{{footer}}</button>
|
|
</span></p>
|
|
<div id="dripSteps" class="drip-steps"></div>
|
|
<p id="dripErr" class="small" style="color:var(--bad)" hidden></p>
|
|
<p style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:14px 0 0">
|
|
<button class="btn small" id="dripSave" type="button">Save sequence</button>
|
|
<button class="btn small sec" id="dripAdd" type="button">+ Add an email</button>
|
|
<button class="btn small sec" id="dripReset" type="button">Reset to defaults</button>
|
|
</p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Rates</h3><span class="sub">credit prices, rewards and limits</span></div>
|
|
<div id="ratesForm" class="rates-form"></div>
|
|
<p id="ratesErr" class="small" style="color:var(--bad)" hidden></p>
|
|
<p style="margin:14px 0 0"><button class="btn small" id="ratesSave" type="button">Save rates</button></p>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-head"><h3>Site settings</h3><span class="sub">public: everything here is served to the browser, never put a secret in it</span></div>
|
|
<div id="siteForm" class="kv-form"></div>
|
|
<p style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:12px 0 0">
|
|
<input id="siteNewKey" placeholder="new setting name" style="max-width:220px">
|
|
<button class="btn small sec" id="siteAddKey" type="button">+ Add setting</button>
|
|
</p>
|
|
<p id="siteErr" class="small" style="color:var(--bad)" hidden></p>
|
|
<p style="margin:14px 0 0"><button class="btn small" id="siteSave" type="button">Save site settings</button></p>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/assets/common.js?v=20260914a"></script>
|
|
<script src="/assets/admin.js?v=20260915a"></script>
|
|
</body>
|
|
</html>
|