WhatsApp-Logger-Self-Hosted-

A privacy-focused, self-hosted...
Log | Files | Refs | Activity | README | LICENSE

root / src / views / pages.js

pages.js (14775B)


      1 const { VERSION } = require('../config');
      2 
      3 // --- SHARED LAYOUT ---
      4 // One small design system so every page (login, status, logs) shares the
      5 // same responsive behavior instead of hand-rolled inline styles per page.
      6 const BASE_STYLES = `
      7     :root {
      8         --accent: #25D366;
      9         --accent-dark: #1da851;
     10         --bg: #f0f2f5;
     11         --card: #ffffff;
     12         --text: #1c1e21;
     13         --muted: #65676b;
     14         --border: #e4e6eb;
     15         --danger: #ff4444;
     16         --radius: 12px;
     17     }
     18     * { box-sizing: border-box; }
     19     body {
     20         margin: 0;
     21         font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
     22         background: var(--bg);
     23         color: var(--text);
     24         min-height: 100vh;
     25         padding: 16px;
     26     }
     27     .page {
     28         max-width: 480px;
     29         margin: 0 auto;
     30         display: flex;
     31         flex-direction: column;
     32         min-height: calc(100vh - 32px);
     33         justify-content: center;
     34         gap: 16px;
     35     }
     36     .card {
     37         background: var(--card);
     38         border-radius: var(--radius);
     39         box-shadow: 0 2px 10px rgba(0,0,0,0.06);
     40         padding: 28px 24px;
     41         width: 100%;
     42     }
     43     .brand {
     44         display: flex;
     45         align-items: center;
     46         justify-content: center;
     47         gap: 8px;
     48         margin-bottom: 4px;
     49     }
     50     .brand-mark {
     51         width: 36px;
     52         height: 36px;
     53         flex-shrink: 0;
     54         display: block;
     55     }
     56     .brand-mark img { width: 100%; height: 100%; display: block; }
     57     .brand-text { text-align: left; }
     58     .brand-text h1 { font-size: 17px; margin: 0; line-height: 1.2; }
     59     .brand-text .version { font-size: 12px; color: var(--muted); }
     60     .subtitle {
     61         text-align: center;
     62         color: var(--muted);
     63         font-size: 13.5px;
     64         margin: 0 0 20px;
     65     }
     66     label { display: block; margin-bottom: 6px; font-size: 13.5px; font-weight: 600; }
     67     input[type="text"], input[type="password"] {
     68         width: 100%;
     69         padding: 11px 12px;
     70         border: 1px solid var(--border);
     71         border-radius: 8px;
     72         font-size: 15px;
     73         background: #fafafa;
     74     }
     75     input[type="text"]:focus, input[type="password"]:focus {
     76         outline: none;
     77         border-color: var(--accent);
     78         background: white;
     79     }
     80     .field { margin-bottom: 14px; }
     81     .checkbox-row {
     82         display: flex;
     83         align-items: center;
     84         font-size: 13.5px;
     85         color: var(--muted);
     86         margin-bottom: 18px;
     87     }
     88     .checkbox-row input { margin-right: 8px; }
     89     button, .btn {
     90         width: 100%;
     91         padding: 12px;
     92         background: var(--accent);
     93         color: white;
     94         border: none;
     95         border-radius: 8px;
     96         font-weight: 600;
     97         font-size: 15px;
     98         cursor: pointer;
     99         text-align: center;
    100         text-decoration: none;
    101         display: inline-block;
    102     }
    103     button:hover, .btn:hover { background: var(--accent-dark); }
    104     .btn-secondary {
    105         background: transparent;
    106         color: var(--text);
    107         border: 1px solid var(--border);
    108     }
    109     .btn-secondary:hover { background: #f5f5f5; }
    110     .btn-danger { background: var(--danger); }
    111     .error-text { color: var(--danger); font-size: 13px; margin-top: 10px; text-align: center; }
    112     .topbar {
    113         display: flex;
    114         justify-content: space-between;
    115         align-items: center;
    116         max-width: 480px;
    117         margin: 0 auto 4px;
    118         width: 100%;
    119     }
    120     .topbar h1 { font-size: 16px; margin: 0; }
    121     .status-badge {
    122         display: inline-flex;
    123         align-items: center;
    124         gap: 6px;
    125         font-size: 13px;
    126         font-weight: 600;
    127         padding: 4px 10px 4px 8px;
    128         border-radius: 999px;
    129         white-space: nowrap;
    130     }
    131     .status-badge .dot { width: 8px; height: 8px; border-radius: 50%; }
    132     .status-connected { background: #e6f8ec; color: #1a7a3d; }
    133     .status-connected .dot { background: #22c55e; }
    134     .status-waiting { background: #fff6e0; color: #92650a; }
    135     .status-waiting .dot { background: #f59e0b; }
    136     .status-init { background: #eee; color: var(--muted); }
    137     .status-init .dot { background: #9ca3af; }
    138     .qr-wrap { text-align: center; margin: 6px 0 4px; }
    139     .qr-wrap img { width: 100%; max-width: 260px; border: 1px solid var(--border); border-radius: 8px; padding: 8px; background: white; }
    140     .hint { color: var(--muted); font-size: 13px; text-align: center; margin: 8px 0 0; }
    141     .section-title { font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin: 0 0 10px; font-weight: 700; }
    142     .link-row {
    143         display: flex;
    144         align-items: center;
    145         justify-content: space-between;
    146         padding: 12px 14px;
    147         border: 1px solid var(--border);
    148         border-radius: 8px;
    149         text-decoration: none;
    150         color: var(--text);
    151         font-size: 14.5px;
    152         font-weight: 600;
    153         margin-bottom: 8px;
    154     }
    155     .link-row:hover { background: #fafafa; }
    156     .link-row .arrow { color: var(--muted); font-weight: 400; }
    157     .endpoint {
    158         display: grid;
    159         grid-template-columns: 52px 1fr;
    160         gap: 10px;
    161         padding: 10px 0;
    162         border-bottom: 1px solid var(--border);
    163         font-size: 13px;
    164     }
    165     .endpoint:last-child { border-bottom: none; }
    166     .method {
    167         font-weight: 700;
    168         font-size: 11px;
    169         padding: 3px 0;
    170         border-radius: 5px;
    171         text-align: center;
    172         height: fit-content;
    173         color: white;
    174     }
    175     .method-get { background: #3b82f6; }
    176     .method-post { background: #8b5cf6; }
    177     .endpoint-path { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; word-break: break-all; }
    178     .endpoint-desc { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
    179     .endpoint-auth { display: inline-block; margin-top: 4px; font-size: 11px; color: var(--muted); background: #f2f3f5; padding: 1px 7px; border-radius: 4px; }
    180     @media (max-width: 400px) {
    181         .card { padding: 22px 18px; }
    182         body { padding: 12px; }
    183     }
    184 `;
    185 
    186 function layout({ title, refresh, content, wide }) {
    187     return `<!DOCTYPE html>
    188 <html lang="en">
    189 <head>
    190     <meta charset="UTF-8">
    191     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    192     <link rel="icon" href="/icon.svg" type="image/svg+xml">
    193     <title>${title}</title>
    194     ${refresh ? `<meta http-equiv="refresh" content="${refresh}">` : ''}
    195     <style>${BASE_STYLES}${wide ? ' .page { max-width: 560px; }' : ''}</style>
    196 </head>
    197 <body>
    198     ${content}
    199 </body>
    200 </html>`;
    201 }
    202 
    203 const brand = `
    204     <div class="brand">
    205         <div class="brand-mark"><img src="/icon.svg" alt="WhatsApp Logger"></div>
    206         <div class="brand-text">
    207             <h1>WhatsApp Logger</h1>
    208             <div class="version">v${VERSION}</div>
    209         </div>
    210     </div>
    211 `;
    212 
    213 function logoutBtn() {
    214     return `<a href="/logout" class="btn btn-secondary" style="width:auto; padding: 8px 16px; font-size: 13px;">Logout</a>`;
    215 }
    216 
    217 // --- LOGIN PAGE ---
    218 function loginPage() {
    219     const content = `
    220         <div class="page">
    221             <div class="card">
    222                 ${brand}
    223                 <p class="subtitle">Sign in to view your archived chats</p>
    224                 <form action="/login" method="POST">
    225                     <div class="field">
    226                         <label>Username</label>
    227                         <input type="text" name="username" autocomplete="username" required>
    228                     </div>
    229                     <div class="field">
    230                         <label>Password</label>
    231                         <input type="password" name="password" autocomplete="current-password" required>
    232                     </div>
    233                     <label class="checkbox-row">
    234                         <input type="checkbox" name="remember" value="yes">
    235                         Keep me logged in for 5 mins
    236                     </label>
    237                     <button type="submit">Log in</button>
    238                 </form>
    239             </div>
    240             <p class="hint">Self-hosted &middot; your data never leaves your own Firebase project</p>
    241         </div>
    242     `;
    243     return layout({ title: 'Login · WhatsApp Logger', content });
    244 }
    245 
    246 function loginFailedPage() {
    247     const content = `
    248         <div class="page">
    249             <div class="card" style="text-align: center;">
    250                 ${brand}
    251                 <p class="error-text" style="margin-bottom: 18px;">Invalid username or password.</p>
    252                 <a class="btn" href="/login">Try again</a>
    253             </div>
    254         </div>
    255     `;
    256     return layout({ title: 'Login failed · WhatsApp Logger', content });
    257 }
    258 
    259 // --- API DOCS (shown on the status page) ---
    260 const ENDPOINTS = [
    261     { method: 'POST', path: '/api/verify', desc: 'Exchange username/password for a bearer token.', auth: 'none' },
    262     { method: 'GET', path: '/api/chats/stream', desc: 'Server-sent events stream of chat list updates.', auth: 'token' },
    263     { method: 'GET', path: '/api/messages/stream', desc: 'Server-sent events stream of messages for a chat (?chatId=&since=).', auth: 'token' },
    264     { method: 'POST', path: '/api/rename', desc: 'Set a custom name for a chat. Body: { id, customName }.', auth: 'token' },
    265     { method: 'GET', path: '/api/export', desc: 'Dump all cached chats and messages as JSON.', auth: 'token' },
    266     { method: 'GET', path: '/logs', desc: 'View server logs. Add ?format=json for raw JSON.', auth: 'token/cookie' },
    267     { method: 'GET', path: '/ping', desc: 'Health check. Always returns 200.', auth: 'none' }
    268 ];
    269 
    270 function apiDocsSection() {
    271     const rows = ENDPOINTS.map(e => `
    272         <div class="endpoint">
    273             <span class="method method-${e.method.toLowerCase()}">${e.method}</span>
    274             <div>
    275                 <div class="endpoint-path">${e.path}</div>
    276                 <div class="endpoint-desc">${e.desc}</div>
    277                 <span class="endpoint-auth">auth: ${e.auth}</span>
    278             </div>
    279         </div>
    280     `).join('');
    281 
    282     return `
    283         <div class="card">
    284             <p class="section-title">API Endpoints</p>
    285             ${rows}
    286         </div>
    287     `;
    288 }
    289 
    290 // --- STATUS PAGE (unified: connected / waiting for QR scan / initializing) ---
    291 function statusBody({ badgeHtml, innerContent }) {
    292     return `
    293         <div class="page">
    294             <div class="topbar">
    295                 <h1>WhatsApp Logger</h1>
    296                 ${logoutBtn()}
    297             </div>
    298             <div class="card">
    299                 ${badgeHtml}
    300                 ${innerContent}
    301             </div>
    302             <div class="card">
    303                 <p class="section-title">Quick links</p>
    304                 <a class="link-row" href="/logs">
    305                     <span>View server logs</span>
    306                     <span class="arrow">&rarr;</span>
    307                 </a>
    308             </div>
    309             ${apiDocsSection()}
    310         </div>
    311     `;
    312 }
    313 
    314 function connectedPage() {
    315     const badgeHtml = `<span class="status-badge status-connected"><span class="dot"></span>Connected</span>`;
    316     const innerContent = `
    317         <p style="margin: 14px 0 0; color: var(--muted); font-size: 14px;">
    318             Linked to WhatsApp and syncing to Firestore in real time.
    319         </p>
    320     `;
    321     return layout({
    322         title: 'Status · WhatsApp Logger',
    323         content: statusBody({ badgeHtml, innerContent })
    324     });
    325 }
    326 
    327 function qrPage(qrImage) {
    328     const badgeHtml = `<span class="status-badge status-waiting"><span class="dot"></span>Waiting for scan</span>`;
    329     const innerContent = `
    330         <div class="qr-wrap">
    331             <img src="${qrImage}" alt="QR Code">
    332         </div>
    333         <p class="hint">Open WhatsApp &rarr; Linked Devices &rarr; Link a Device, then scan this code. Refreshes every 5 seconds.</p>
    334     `;
    335     return layout({
    336         title: 'Scan to link · WhatsApp Logger',
    337         refresh: 5,
    338         content: statusBody({ badgeHtml, innerContent })
    339     });
    340 }
    341 
    342 function qrErrorPage() {
    343     const content = `
    344         <div class="page">
    345             <div class="card" style="text-align: center;">
    346                 ${brand}
    347                 <p class="error-text">Couldn't generate the QR code. Refresh to try again.</p>
    348             </div>
    349         </div>
    350     `;
    351     return layout({ title: 'Error · WhatsApp Logger', content });
    352 }
    353 
    354 function initializingPage() {
    355     const badgeHtml = `<span class="status-badge status-init"><span class="dot"></span>Initializing</span>`;
    356     const innerContent = `
    357         <p style="margin: 14px 0 0; color: var(--muted); font-size: 14px;">
    358             Connecting to WhatsApp and restoring the saved session. This page refreshes automatically.
    359         </p>
    360     `;
    361     return layout({
    362         title: 'Initializing · WhatsApp Logger',
    363         refresh: 2,
    364         content: statusBody({ badgeHtml, innerContent })
    365     });
    366 }
    367 
    368 // --- LOGS PAGE ---
    369 function logsPage(logBuffer) {
    370     const logLines = logBuffer.map(l => {
    371         const time = new Date(l.timestamp).toLocaleTimeString();
    372         const color = l.level === 'error' ? '#ff6b6b' : '#a9dc76';
    373         return `<div style="color: ${color}">[${time}] [${l.level.toUpperCase()}] ${l.message}</div>`;
    374     }).join('');
    375 
    376     const content = `
    377         <div style="max-width: 100%;">
    378             <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: 12px; flex-wrap: wrap; gap: 8px;">
    379                 <h2 style="color: #fff; margin: 0; font-size: 18px;">System Logs</h2>
    380                 <a href="/" style="color: #a9dc76; font-size: 13px; text-decoration: none;">&larr; Back to status</a>
    381             </div>
    382             <div class="log-container">
    383                 ${logLines.length > 0 ? logLines : '<div>No logs yet...</div>'}
    384             </div>
    385         </div>
    386     `;
    387 
    388     return `<!DOCTYPE html>
    389 <html lang="en">
    390 <head>
    391     <meta charset="UTF-8">
    392     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    393     <link rel="icon" href="/icon.svg" type="image/svg+xml">
    394     <meta http-equiv="refresh" content="5">
    395     <title>System Logs</title>
    396     <style>
    397         * { box-sizing: border-box; }
    398         body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #1e1e1e; color: #d4d4d4; padding: 16px; margin: 0; }
    399         .log-container { background: #000; padding: 14px; border-radius: 8px; overflow-x: auto; max-width: 100%; white-space: pre-wrap; word-break: break-word; font-size: 13px; line-height: 1.6; }
    400         @media (max-width: 400px) { body { padding: 10px; } .log-container { padding: 10px; font-size: 12px; } }
    401     </style>
    402 </head>
    403 <body onload="window.scrollTo(0,document.body.scrollHeight);">
    404     ${content}
    405 </body>
    406 </html>`;
    407 }
    408 
    409 module.exports = {
    410     logsPage,
    411     loginPage,
    412     loginFailedPage,
    413     connectedPage,
    414     qrPage,
    415     qrErrorPage,
    416     initializingPage
    417 };
© notamitgamer • Site Built: 2026-09-05 01:53:16 UTC • git-mirror commit: c170d72 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror