<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="theme-color" content="#101418" /> <meta name="robots" content="noindex, nofollow" /> <title>R.E.X Dev Portal</title> <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23D0BCFF'/%3E%3Ctext x='16' y='22' font-family='Inter,sans-serif' font-weight='800' font-size='16' text-anchor='middle' fill='%23381E72'%3ER%3C/text%3E%3C/svg%3E" /> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&family=Roboto:wght@400;500;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/icon?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" rel="stylesheet" /> <!-- Firebase Auth --> <script src="/__/firebase/10.11.1/firebase-app-compat.js"></script> <script src="/__/firebase/10.11.1/firebase-auth-compat.js"></script> <script src="/__/firebase/init.js"></script> <style> :root{ --md-background:#101418; --md-surface-1:#161B22; --md-surface-2:#1E242C; --md-surface-3:#242C35; --md-on-surface:#E6E1E5; --md-on-surface-variant:#CAC4D0; --md-outline:#49454F; --md-outline-variant:#2A2F38; --md-primary:#D0BCFF; --md-on-primary:#381E72; --md-success:#7FE4A7; --md-warning:#FFD479; --md-error:#F2B8B5; --radius:14px; --radius-lg:22px; --ease-standard: cubic-bezier(0.2,0,0,1); --font-display:'Google Sans','Roboto',system-ui,sans-serif; } *{box-sizing:border-box;} html,body{margin:0;padding:0;background:var(--md-background);color:var(--md-on-surface);font-family:'Roboto',system-ui,sans-serif;-webkit-font-smoothing:antialiased;} body{min-height:100vh;} a{color:var(--md-primary);text-decoration:none;} a:hover{text-decoration:underline;} .app-bar{ position:sticky;top:0;z-index:40; background:rgba(16,20,24,0.88); backdrop-filter:saturate(160%) blur(14px); border-bottom:1px solid var(--md-outline-variant); } .app-bar__inner{ max-width:1180px;margin:0 auto;padding:14px 20px; display:flex;align-items:center;justify-content:space-between;gap:16px; } .brand{display:flex;align-items:center;gap:12px;} .brand__logo{ width:36px;height:36px;border-radius:10px;background:var(--md-primary); color:var(--md-on-primary);font-family:var(--font-display);font-weight:800; display:flex;align-items:center;justify-content:center;font-size:18px; } .brand__name{font-family:var(--font-display);font-weight:700;font-size:17px;letter-spacing:0.01em;} .brand__tag{font-size:11px;color:var(--md-on-surface-variant);letter-spacing:0.04em;text-transform:uppercase;} main{max-width:1180px;margin:0 auto;padding:32px 20px 120px;} .page-head{margin-bottom:28px;} .page-head__title{ font-family:var(--font-display);font-size:40px;line-height:1.1;margin:0 0 10px; font-weight:700;letter-spacing:-0.02em; } .page-head__sub{color:var(--md-on-surface-variant);font-size:16px;max-width:720px;line-height:1.55;margin:0;} .summary-grid{ display:grid;gap:14px;margin:24px 0 32px; } .stat-card{ background:var(--md-surface-1);border:1px solid var(--md-outline-variant); border-radius:var(--radius);padding:18px; } .stat-card__label{font-size:11px;font-family:var(--font-display);font-weight:600; letter-spacing:0.08em;text-transform:uppercase;color:var(--md-on-surface-variant);margin-bottom:8px;} .stat-card__value{font-family:var(--font-display);font-size:32px;font-weight:700;letter-spacing:-0.02em;line-height:1;} .stat-card__unit{font-size:14px;color:var(--md-on-surface-variant);font-weight:500;margin-left:4px;} .btn{ display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border-radius:100px; font-family:var(--font-display);font-size:14px;font-weight:500;letter-spacing:0.01em; border:none;cursor:pointer;white-space:nowrap; transition:transform 160ms var(--ease-standard), background 160ms var(--ease-standard), box-shadow 160ms var(--ease-standard); } .btn .material-symbols-rounded{font-size:18px;} .btn--filled{background:var(--md-primary);color:var(--md-on-primary);padding:12px 24px;font-weight:600;} .btn--filled:hover{background:#D9C7FF;box-shadow:0 8px 24px rgba(208,188,255,0.2);transform:translateY(-1px);} .btn--ghost{background:transparent;color:var(--md-on-surface-variant);} .btn--ghost:hover{color:var(--md-on-surface);} .btn[disabled]{opacity:0.45;cursor:not-allowed;transform:none;} /* Toast */ .toast{ position:fixed;top:20px;left:50%;transform:translateX(-50%) translateY(-24px); background:var(--md-surface-2);border:1px solid var(--md-outline-variant); border-radius:14px;padding:14px 22px; font-family:var(--font-display);font-size:14px;font-weight:500;color:var(--md-on-surface); box-shadow:0 12px 32px rgba(0,0,0,0.4);opacity:0;pointer-events:none;z-index:100; transition:opacity 200ms var(--ease-standard), transform 200ms var(--ease-standard); display:flex;align-items:center;gap:10px; } .toast[data-show="true"]{opacity:1;transform:translateX(-50%) translateY(0);} .toast--success{border-color:rgba(127,228,167,0.4);} .toast--error{border-color:rgba(242,184,181,0.4);} @media (max-width:820px){ .summary-grid{grid-template-columns:repeat(2,1fr);} .page-head__title{font-size:30px;} } </style> </head> <body> <header class="app-bar"> <div class="app-bar__inner"> <div class="brand"> <div class="brand__logo">D</div> <div> <div class="brand__name">Dev Portal</div> <span class="brand__tag">Admin · Analytics · Ops</span> </div> </div> <div style="display:flex;align-items:center;gap:10px;"> <a class="btn btn--ghost" href="/review.html"> <span class="material-symbols-rounded">science</span> Pending Reviews </a> <a class="btn btn--ghost" href="/index.html"> <span class="material-symbols-rounded">open_in_new</span> Live site </a> </div> </div> </header> <main id="content"> <div id="tabview-analytics"> <div class="page-head" style="margin-bottom: 24px;"> <h1 class="page-head__title" style="font-size:28px;">Analytics & Broadcast</h1> <p class="page-head__sub">Site traffic and newsletter tools.</p> </div> <div class="summary-grid" style="margin-bottom: 32px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));"> <div class="stat-card"> <div class="stat-card__label">Total Visits</div> <div class="stat-card__value" id="stat-visits">--</div> </div> <div class="stat-card"> <div class="stat-card__label">Subscribers</div> <div class="stat-card__value" id="stat-subscribers">--</div> </div> <div class="stat-card"> <div class="stat-card__label">Active Models</div> <div class="stat-card__value" id="stat-models">--</div> </div> </div> <div class="change-card" style="margin-bottom:24px; padding:24px; background:var(--md-surface-2); border-radius:var(--radius); border:1px solid var(--md-outline-variant);"> <h3 style="font-family:var(--font-display); font-size:18px; margin:0 0 8px; color:var(--md-on-surface);">Broadcast Weekly Digest</h3> <p style="color:var(--md-on-surface-variant); font-size:14px; margin:0 0 20px; line-height:1.5;">Send an email to all confirmed subscribers with the current Top 5 AI models. Uses Resend Batch API.</p> <div style="display:flex; gap:12px;"> <button class="btn btn--ghost" onclick="sendBroadcast(true)" id="btn-broadcast-test"> <span class="material-symbols-rounded">mail</span> Send Test to Dev </button> <button class="btn btn--filled" onclick="sendBroadcast(false)" id="btn-broadcast-live" style="background:#D93025; color:#fff;"> <span class="material-symbols-rounded">campaign</span> Broadcast to All </button> </div> </div> </div> </main> <style> .login-overlay { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px; background: rgba(8,10,14,0.72); backdrop-filter: blur(8px); transition: opacity 220ms var(--ease-standard); overflow-y: auto; opacity: 1; } .login-overlay[aria-hidden="true"] { opacity: 0; pointer-events: none; visibility: hidden; } .login-modal { position: relative; width: 100%; max-width: 400px; background: var(--md-surface-2); border: 1px solid var(--md-outline-variant); border-radius: var(--radius-lg); padding: 32px; box-shadow: 0 24px 80px rgba(0,0,0,0.45); transform: translateY(0); transition: transform 220ms var(--ease-standard); } .login-overlay[aria-hidden="true"] .login-modal { transform: translateY(12px); } .login-modal__close { position: absolute; top: 14px; right: 14px; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; background: transparent; color: var(--md-on-surface-variant); border: none; border-radius: 50%; cursor: pointer; } .login-modal__close:hover { background: rgba(255,255,255,0.08); color: var(--md-on-surface); } .login-modal__title { font-family: var(--font-display); font-size: 22px; font-weight: 500; margin: 0 0 6px; } .login-modal__sub { font-size: 13px; color: var(--md-on-surface-variant); margin: 0 0 24px; } .social-auth { display: flex; flex-direction: column; gap: 8px; margin-top: 24px; } .social-auth__btn { width: 100%; justify-content: center; background: transparent; border: 1px solid var(--md-outline-variant); color: var(--md-on-surface); padding: 12px; border-radius: 100px; display: flex; align-items: center; gap: 8px; font-weight: 500; cursor: pointer; transition: background 160ms; } .social-auth__btn:hover { background: rgba(255,255,255,0.06); } .auth-divider { display: flex; align-items: center; text-align: center; color: var(--md-on-surface-variant); font-size: 12px; margin: 24px 0; text-transform: uppercase; letter-spacing: 0.08em; } .auth-divider::before, .auth-divider::after { content: ''; flex: 1; border-bottom: 1px solid var(--md-outline-variant); } .auth-divider span { padding: 0 12px; } .field { display: flex; flex-direction: column; gap: 4px; } .field__label { font-size: 11px; font-family: var(--font-display); font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--md-on-surface-variant); } .field__input { background: var(--md-surface-1); border: 1px solid var(--md-outline-variant); border-radius: 10px; color: var(--md-on-surface); padding: 10px 12px; font-family: 'Roboto Mono', monospace; font-size: 13px; } .field__input:focus { outline: 2px solid var(--md-primary); outline-offset: -1px; border-color: transparent; } </style> <div id="login-overlay" class="login-overlay" aria-hidden="true"> <div class="login-modal" role="dialog" aria-modal="true" aria-labelledby="login-title"> <button class="login-modal__close" onclick="closeLoginModal()" aria-label="Close"> <span class="material-symbols-rounded">close</span> </button> <h2 id="login-title" class="login-modal__title">Dev Access Only</h2> <p class="login-modal__sub">Log in with an authorized dev account.</p> <div class="social-auth"> <button class="social-auth__btn" onclick="doGoogleLogin()"> <svg width="18" height="18" viewBox="0 0 24 24"><path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.3-1.92 3.28-4.74 3.28-8.09z"/><path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/><path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/><path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/></svg> Continue with Google </button> </div> <div class="auth-divider"><span>or use email</span></div> <form id="loginForm" onsubmit="doEmailLogin(event)"> <label class="field" style="margin-bottom: 12px;"> <span class="field__label">Email</span> <input class="field__input" type="email" id="loginEmail" required placeholder="you@example.com" style="width:100%; box-sizing:border-box;"> </label> <label class="field"> <span class="field__label">Password</span> <input class="field__input" type="password" id="loginPassword" required placeholder="" " " " " " " " " style="width:100%; box-sizing:border-box;"> </label> <button type="submit" class="btn btn--filled" style="width:100%; justify-content:center; margin-top:16px;"> Log in </button> <p id="loginError" style="color:var(--md-error); font-size:13px; margin-top:12px; background:rgba(242,184,181,0.1); padding:8px 12px; border-radius:6px; display:none;"></p> </form> </div> </div> <div class="toast" id="toast"><span class="material-symbols-rounded">check_circle</span><span id="toast-msg"></span></div> <script> /* DEV GATE  restricted to reece@wildfirelogs.net */ (function devGate(){ window.openLoginModal = function() { const overlay = document.getElementById('login-overlay'); if (!overlay) return; document.getElementById('loginError').style.display = 'none'; document.getElementById('loginForm').reset(); overlay.setAttribute('aria-hidden', 'false'); document.body.style.overflow = 'hidden'; }; window.closeLoginModal = function() { const overlay = document.getElementById('login-overlay'); if (!overlay) return; overlay.setAttribute('aria-hidden', 'true'); document.body.style.overflow = ''; }; window.doDevLogin = function() { if (typeof firebase === 'undefined' || !firebase.auth) { alert("Firebase is still loading or could not be reached. Try refreshing the page."); return; } openLoginModal(); }; window.doGoogleLogin = function() { const provider = new firebase.auth.GoogleAuthProvider(); firebase.auth().signInWithPopup(provider).then(() => { closeLoginModal(); }).catch(err => { if (err.code !== 'auth/popup-closed-by-user') { const errEl = document.getElementById('loginError'); errEl.textContent = err.message; errEl.style.display = 'block'; } }); }; window.doEmailLogin = function(ev) { ev.preventDefault(); const email = document.getElementById('loginEmail').value; const password = document.getElementById('loginPassword').value; const btn = ev.target.querySelector('button[type="submit"]'); const ogText = btn.textContent; btn.textContent = "Logging in..."; firebase.auth().signInWithEmailAndPassword(email, password).then(() => { closeLoginModal(); btn.textContent = ogText; }).catch(err => { btn.textContent = ogText; const errEl = document.getElementById('loginError'); errEl.textContent = err.message; errEl.style.display = 'block'; }); }; document.addEventListener('DOMContentLoaded', () => { const content = document.getElementById('content'); if (content) content.style.opacity = '0'; const checkAuth = setInterval(() => { if (typeof firebase !== 'undefined' && firebase.auth) { clearInterval(checkAuth); firebase.auth().onAuthStateChanged(user => { if (!user || user.email !== 'reece@wildfirelogs.net') { document.body.innerHTML = '<div style="min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--md-surface);color:var(--md-on-surface);font-family:\'Google Sans\',sans-serif;text-align:center;padding:40px;"><div style="max-width:420px;"><div style="font-size:56px;margin-bottom:16px;">=ØÝ</div><h1 style="font-size:22px;margin:0 0 10px;font-weight:600;">Dev Access Only</h1><p style="color:var(--md-on-surface-variant);margin:0 0 24px;line-height:1.6;">This page is restricted to R.E.X admins. Please log in with an authorized dev account.</p><button onclick="doDevLogin()" class="btn btn--filled" style="display:inline-flex;padding:12px 24px;background:var(--md-primary);color:var(--md-on-primary);text-decoration:none;border-radius:100px;font-weight:600;font-size:15px;"><span class="material-symbols-rounded">login</span> Log In</button></div></div>'; } else { if (content) { content.style.opacity = '1'; loadDevStats(); } } }); } }, 100); }); })(); let toastTimer; function toast(msg, kind = 'success'){ const t = document.getElementById('toast'); const m = document.getElementById('toast-msg'); m.textContent = msg; t.className = 'toast toast--' + kind; t.dataset.show = 'true'; clearTimeout(toastTimer); toastTimer = setTimeout(() => { t.dataset.show = 'false'; }, 2200); } async function loadDevStats() { try { const user = firebase.auth().currentUser; if (!user) return; const token = await user.getIdToken(); const res = await fetch('/api/getDevStats', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ data: {} }) }); const json = await res.json(); if (json.result) { document.getElementById('stat-visits').textContent = json.result.visits || 0; document.getElementById('stat-subscribers').textContent = json.result.subscribers || 0; document.getElementById('stat-models').textContent = json.result.models || 0; } } catch (e) { console.error('Failed to load dev stats', e); } } async function sendBroadcast(testOnly) { const btn = document.getElementById(testOnly ? 'btn-broadcast-test' : 'btn-broadcast-live'); const originalText = btn.innerHTML; btn.innerHTML = '<span class="material-symbols-rounded">hourglass_empty</span> Sending...'; btn.disabled = true; try { const user = firebase.auth().currentUser; const token = await user.getIdToken(); const res = await fetch('/api/broadcastDigest', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ data: { testOnly } }) }); const json = await res.json(); if (json.result && json.result.ok) { toast(json.result.message); } else { toast('Broadcast failed: ' + (json.error?.message || 'Unknown error'), 'error'); } } catch (e) { toast('Error sending broadcast.', 'error'); console.error(e); } finally { btn.innerHTML = originalText; btn.disabled = false; } } </script> </body> </html>