commit 6585e909d5214be8b589547a68b1d5a99314a379
parent 922a4ea40eba9e8fb8689981dfe7f214dfc6dc30
Author: notamitgamer2 <notamitgamer2@users.noreply.github.com>
Date: Wed, 26 Aug 2026 04:25:04 +0000
Redesign login/status pages: mobile-responsive, richer status page
- Add a shared responsive layout (viewport meta, flex-based card
container, 400px breakpoint) used by every server-rendered page.
Previously none of the pages had a viewport meta tag, so they
rendered zoomed-out and unusably small on phones.
- Login page: adds a small brand mark/header and a footer hint line
instead of a bare form, while staying to one card and no extra
chrome — still minimal, just not empty.
- Status page (connected / waiting-for-QR / initializing) is now
unified behind one statusBody() layout with:
- a colored status badge (green connected / amber waiting / gray
initializing) instead of plain text
- a 'Quick links' card linking to /logs
- an 'API Endpoints' card documenting all 7 routes (method, path,
description, auth requirement) for people deploying their own
instance
- Logs page gets the same viewport fix plus a 'Back to status' link
and word-wrapping so long log lines don't overflow on narrow
screens.
- No behavior/route changes — same exported function names, same
routes call them the same way.
Verified: node --check passes, booted the server locally and pulled
rendered HTML for /login, failed /login, /(status), and /logs — all
include the viewport meta tag and the expected markup (status badge,
quick-links section, all 7 documented endpoints).
Diffstat:
| M | src/views/pages.js | | | 459 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- |
1 file changed, 381 insertions(+), 78 deletions(-)
diff --git a/src/views/pages.js b/src/views/pages.js
@@ -1,110 +1,413 @@
const { VERSION } = require('../config');
-function logsPage(logBuffer) {
- const logLines = logBuffer.map(l => {
- const time = new Date(l.timestamp).toLocaleTimeString();
- const color = l.level === 'error' ? '#ff6b6b' : '#a9dc76';
- return `<div style="color: ${color}">[${time}] [${l.level.toUpperCase()}] ${l.message}</div>`;
- }).join('');
+// --- SHARED LAYOUT ---
+// One small design system so every page (login, status, logs) shares the
+// same responsive behavior instead of hand-rolled inline styles per page.
+const BASE_STYLES = `
+ :root {
+ --accent: #25D366;
+ --accent-dark: #1da851;
+ --bg: #f0f2f5;
+ --card: #ffffff;
+ --text: #1c1e21;
+ --muted: #65676b;
+ --border: #e4e6eb;
+ --danger: #ff4444;
+ --radius: 12px;
+ }
+ * { box-sizing: border-box; }
+ body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ background: var(--bg);
+ color: var(--text);
+ min-height: 100vh;
+ padding: 16px;
+ }
+ .page {
+ max-width: 480px;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ min-height: calc(100vh - 32px);
+ justify-content: center;
+ gap: 16px;
+ }
+ .card {
+ background: var(--card);
+ border-radius: var(--radius);
+ box-shadow: 0 2px 10px rgba(0,0,0,0.06);
+ padding: 28px 24px;
+ width: 100%;
+ }
+ .brand {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ margin-bottom: 4px;
+ }
+ .brand-mark {
+ width: 36px;
+ height: 36px;
+ border-radius: 10px;
+ background: var(--accent);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-weight: 700;
+ font-size: 18px;
+ flex-shrink: 0;
+ }
+ .brand-text { text-align: left; }
+ .brand-text h1 { font-size: 17px; margin: 0; line-height: 1.2; }
+ .brand-text .version { font-size: 12px; color: var(--muted); }
+ .subtitle {
+ text-align: center;
+ color: var(--muted);
+ font-size: 13.5px;
+ margin: 0 0 20px;
+ }
+ label { display: block; margin-bottom: 6px; font-size: 13.5px; font-weight: 600; }
+ input[type="text"], input[type="password"] {
+ width: 100%;
+ padding: 11px 12px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ font-size: 15px;
+ background: #fafafa;
+ }
+ input[type="text"]:focus, input[type="password"]:focus {
+ outline: none;
+ border-color: var(--accent);
+ background: white;
+ }
+ .field { margin-bottom: 14px; }
+ .checkbox-row {
+ display: flex;
+ align-items: center;
+ font-size: 13.5px;
+ color: var(--muted);
+ margin-bottom: 18px;
+ }
+ .checkbox-row input { margin-right: 8px; }
+ button, .btn {
+ width: 100%;
+ padding: 12px;
+ background: var(--accent);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-weight: 600;
+ font-size: 15px;
+ cursor: pointer;
+ text-align: center;
+ text-decoration: none;
+ display: inline-block;
+ }
+ button:hover, .btn:hover { background: var(--accent-dark); }
+ .btn-secondary {
+ background: transparent;
+ color: var(--text);
+ border: 1px solid var(--border);
+ }
+ .btn-secondary:hover { background: #f5f5f5; }
+ .btn-danger { background: var(--danger); }
+ .error-text { color: var(--danger); font-size: 13px; margin-top: 10px; text-align: center; }
+ .topbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ max-width: 480px;
+ margin: 0 auto 4px;
+ width: 100%;
+ }
+ .topbar h1 { font-size: 16px; margin: 0; }
+ .status-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+ font-weight: 600;
+ padding: 4px 10px 4px 8px;
+ border-radius: 999px;
+ white-space: nowrap;
+ }
+ .status-badge .dot { width: 8px; height: 8px; border-radius: 50%; }
+ .status-connected { background: #e6f8ec; color: #1a7a3d; }
+ .status-connected .dot { background: #22c55e; }
+ .status-waiting { background: #fff6e0; color: #92650a; }
+ .status-waiting .dot { background: #f59e0b; }
+ .status-init { background: #eee; color: var(--muted); }
+ .status-init .dot { background: #9ca3af; }
+ .qr-wrap { text-align: center; margin: 6px 0 4px; }
+ .qr-wrap img { width: 100%; max-width: 260px; border: 1px solid var(--border); border-radius: 8px; padding: 8px; background: white; }
+ .hint { color: var(--muted); font-size: 13px; text-align: center; margin: 8px 0 0; }
+ .section-title { font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); margin: 0 0 10px; font-weight: 700; }
+ .link-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 14px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ text-decoration: none;
+ color: var(--text);
+ font-size: 14.5px;
+ font-weight: 600;
+ margin-bottom: 8px;
+ }
+ .link-row:hover { background: #fafafa; }
+ .link-row .arrow { color: var(--muted); font-weight: 400; }
+ .endpoint {
+ display: grid;
+ grid-template-columns: 52px 1fr;
+ gap: 10px;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--border);
+ font-size: 13px;
+ }
+ .endpoint:last-child { border-bottom: none; }
+ .method {
+ font-weight: 700;
+ font-size: 11px;
+ padding: 3px 0;
+ border-radius: 5px;
+ text-align: center;
+ height: fit-content;
+ color: white;
+ }
+ .method-get { background: #3b82f6; }
+ .method-post { background: #8b5cf6; }
+ .endpoint-path { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; word-break: break-all; }
+ .endpoint-desc { color: var(--muted); font-size: 12.5px; margin-top: 2px; }
+ .endpoint-auth { display: inline-block; margin-top: 4px; font-size: 11px; color: var(--muted); background: #f2f3f5; padding: 1px 7px; border-radius: 4px; }
+ @media (max-width: 400px) {
+ .card { padding: 22px 18px; }
+ body { padding: 12px; }
+ }
+`;
- return `
- <html>
- <head>
- <meta http-equiv="refresh" content="5">
- <title>System Logs</title>
- <style>
- body { font-family: monospace; background: #1e1e1e; color: #d4d4d4; padding: 20px; }
- .log-container { background: #000; padding: 15px; border-radius: 5px; overflow-x: auto; max-width: 100%; white-space: pre-wrap; font-size: 14px; line-height: 1.5; }
- </style>
- </head>
- <body onload="window.scrollTo(0,document.body.scrollHeight);">
- <h2 style="color: #fff; margin-top: 0;">System Logs</h2>
- <div class="log-container">
- ${logLines.length > 0 ? logLines : '<div>No logs yet...</div>'}
- </div>
- </body>
- </html>
- `;
+function layout({ title, refresh, content, wide }) {
+ return `<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+ <title>${title}</title>
+ ${refresh ? `<meta http-equiv="refresh" content="${refresh}">` : ''}
+ <style>${BASE_STYLES}${wide ? ' .page { max-width: 560px; }' : ''}</style>
+</head>
+<body>
+ ${content}
+</body>
+</html>`;
}
+const brand = `
+ <div class="brand">
+ <div class="brand-mark">W</div>
+ <div class="brand-text">
+ <h1>WhatsApp Logger</h1>
+ <div class="version">v${VERSION}</div>
+ </div>
+ </div>
+`;
+
+function logoutBtn() {
+ return `<a href="/logout" class="btn btn-secondary" style="width:auto; padding: 8px 16px; font-size: 13px;">Logout</a>`;
+}
+
+// --- LOGIN PAGE ---
function loginPage() {
- return `
- <html>
- <body style="font-family: sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5;">
- <form action="/login" method="POST" style="background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 300px;">
- <h2 style="margin-top: 0; text-align: center;">WhatsApp Logger <span style="font-size: 14px; font-weight: normal; color: #666;">v${VERSION}</span></h2>
- <div style="margin-bottom: 1rem;">
- <label style="display: block; margin-bottom: 0.5rem;">Username</label>
- <input type="text" name="username" required style="width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;">
- </div>
- <div style="margin-bottom: 1rem;">
- <label style="display: block; margin-bottom: 0.5rem;">Password</label>
- <input type="password" name="password" required style="width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box;">
+ const content = `
+ <div class="page">
+ <div class="card">
+ ${brand}
+ <p class="subtitle">Sign in to view your archived chats</p>
+ <form action="/login" method="POST">
+ <div class="field">
+ <label>Username</label>
+ <input type="text" name="username" autocomplete="username" required>
</div>
- <div style="margin-bottom: 1rem;">
- <label style="display: flex; align-items: center; font-size: 0.9rem;">
- <input type="checkbox" name="remember" value="yes" style="margin-right: 0.5rem;">
- Keep me logged in for 5 mins
- </label>
+ <div class="field">
+ <label>Password</label>
+ <input type="password" name="password" autocomplete="current-password" required>
</div>
- <button type="submit" style="width: 100%; padding: 0.75rem; background: #25D366; color: white; border: none; border-radius: 4px; font-weight: bold; cursor: pointer;">Login</button>
+ <label class="checkbox-row">
+ <input type="checkbox" name="remember" value="yes">
+ Keep me logged in for 5 mins
+ </label>
+ <button type="submit">Log in</button>
</form>
- </body>
- </html>
+ </div>
+ <p class="hint">Self-hosted · your data never leaves your own Firebase project</p>
+ </div>
`;
+ return layout({ title: 'Login · WhatsApp Logger', content });
}
function loginFailedPage() {
- return 'Invalid credentials. <a href="/login">Try again</a>';
+ const content = `
+ <div class="page">
+ <div class="card" style="text-align: center;">
+ ${brand}
+ <p class="error-text" style="margin-bottom: 18px;">Invalid username or password.</p>
+ <a class="btn" href="/login">Try again</a>
+ </div>
+ </div>
+ `;
+ return layout({ title: 'Login failed · WhatsApp Logger', content });
}
-const logoutBtn = `<a href="/logout" style="position: absolute; top: 10px; right: 10px; padding: 8px 16px; background: #ff4444; color: white; text-decoration: none; border-radius: 4px; font-size: 14px;">Logout</a>`;
+// --- API DOCS (shown on the status page) ---
+const ENDPOINTS = [
+ { method: 'POST', path: '/api/verify', desc: 'Exchange username/password for a bearer token.', auth: 'none' },
+ { method: 'GET', path: '/api/chats/stream', desc: 'Server-sent events stream of chat list updates.', auth: 'token' },
+ { method: 'GET', path: '/api/messages/stream', desc: 'Server-sent events stream of messages for a chat (?chatId=&since=).', auth: 'token' },
+ { method: 'POST', path: '/api/rename', desc: 'Set a custom name for a chat. Body: { id, customName }.', auth: 'token' },
+ { method: 'GET', path: '/api/export', desc: 'Dump all cached chats and messages as JSON.', auth: 'token' },
+ { method: 'GET', path: '/logs', desc: 'View server logs. Add ?format=json for raw JSON.', auth: 'token/cookie' },
+ { method: 'GET', path: '/ping', desc: 'Health check. Always returns 200.', auth: 'none' }
+];
+
+function apiDocsSection() {
+ const rows = ENDPOINTS.map(e => `
+ <div class="endpoint">
+ <span class="method method-${e.method.toLowerCase()}">${e.method}</span>
+ <div>
+ <div class="endpoint-path">${e.path}</div>
+ <div class="endpoint-desc">${e.desc}</div>
+ <span class="endpoint-auth">auth: ${e.auth}</span>
+ </div>
+ </div>
+ `).join('');
-function connectedPage() {
return `
- <html>
- <body style="font-family: sans-serif; text-align: center; padding-top: 50px; background-color: #f0f2f5;">
- ${logoutBtn}
- <div style="background: white; padding: 40px; border-radius: 10px; display: inline-block; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
- <h2 style="color: green;">System Operational</h2>
- <p style="color: #555;">Connected to WhatsApp. State synced to Firestore.</p>
- <p style="color: #999; font-size: 12px;">Back-end Service</p>
- </div>
- </body>
- </html>
+ <div class="card">
+ <p class="section-title">API Endpoints</p>
+ ${rows}
+ </div>
`;
}
-function qrPage(qrImage) {
+// --- STATUS PAGE (unified: connected / waiting for QR scan / initializing) ---
+function statusBody({ badgeHtml, innerContent }) {
return `
- <html>
- <head><meta http-equiv="refresh" content="5"></head>
- <body style="font-family: sans-serif; text-align: center; padding-top: 50px; background-color: #f0f2f5;">
- ${logoutBtn}
- <div style="background: white; padding: 40px; border-radius: 10px; display: inline-block; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
- <h2>Scan to Link</h2>
- <img src="${qrImage}" alt="QR Code" />
- <p style="color: #666;">Refreshes every 5 seconds...</p>
- </div>
- </body>
- </html>
+ <div class="page">
+ <div class="topbar">
+ <h1>WhatsApp Logger</h1>
+ ${logoutBtn()}
+ </div>
+ <div class="card">
+ ${badgeHtml}
+ ${innerContent}
+ </div>
+ <div class="card">
+ <p class="section-title">Quick links</p>
+ <a class="link-row" href="/logs">
+ <span>View server logs</span>
+ <span class="arrow">→</span>
+ </a>
+ </div>
+ ${apiDocsSection()}
+ </div>
+ `;
+}
+
+function connectedPage() {
+ const badgeHtml = `<span class="status-badge status-connected"><span class="dot"></span>Connected</span>`;
+ const innerContent = `
+ <p style="margin: 14px 0 0; color: var(--muted); font-size: 14px;">
+ Linked to WhatsApp and syncing to Firestore in real time.
+ </p>
+ `;
+ return layout({
+ title: 'Status · WhatsApp Logger',
+ content: statusBody({ badgeHtml, innerContent })
+ });
+}
+
+function qrPage(qrImage) {
+ const badgeHtml = `<span class="status-badge status-waiting"><span class="dot"></span>Waiting for scan</span>`;
+ const innerContent = `
+ <div class="qr-wrap">
+ <img src="${qrImage}" alt="QR Code">
+ </div>
+ <p class="hint">Open WhatsApp → Linked Devices → Link a Device, then scan this code. Refreshes every 5 seconds.</p>
`;
+ return layout({
+ title: 'Scan to link · WhatsApp Logger',
+ refresh: 5,
+ content: statusBody({ badgeHtml, innerContent })
+ });
}
function qrErrorPage() {
- return "Error generating QR.";
+ const content = `
+ <div class="page">
+ <div class="card" style="text-align: center;">
+ ${brand}
+ <p class="error-text">Couldn't generate the QR code. Refresh to try again.</p>
+ </div>
+ </div>
+ `;
+ return layout({ title: 'Error · WhatsApp Logger', content });
}
function initializingPage() {
- return `
- <html>
- <head><meta http-equiv="refresh" content="2"></head>
- <body style="font-family: sans-serif; text-align: center; padding-top: 50px;">
- <p>Initializing connection or restoring auth state... please wait.</p>
- ${logoutBtn}
- </body>
- </html>
+ const badgeHtml = `<span class="status-badge status-init"><span class="dot"></span>Initializing</span>`;
+ const innerContent = `
+ <p style="margin: 14px 0 0; color: var(--muted); font-size: 14px;">
+ Connecting to WhatsApp and restoring the saved session. This page refreshes automatically.
+ </p>
`;
+ return layout({
+ title: 'Initializing · WhatsApp Logger',
+ refresh: 2,
+ content: statusBody({ badgeHtml, innerContent })
+ });
+}
+
+// --- LOGS PAGE ---
+function logsPage(logBuffer) {
+ const logLines = logBuffer.map(l => {
+ const time = new Date(l.timestamp).toLocaleTimeString();
+ const color = l.level === 'error' ? '#ff6b6b' : '#a9dc76';
+ return `<div style="color: ${color}">[${time}] [${l.level.toUpperCase()}] ${l.message}</div>`;
+ }).join('');
+
+ const content = `
+ <div style="max-width: 100%;">
+ <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: 12px; flex-wrap: wrap; gap: 8px;">
+ <h2 style="color: #fff; margin: 0; font-size: 18px;">System Logs</h2>
+ <a href="/" style="color: #a9dc76; font-size: 13px; text-decoration: none;">← Back to status</a>
+ </div>
+ <div class="log-container">
+ ${logLines.length > 0 ? logLines : '<div>No logs yet...</div>'}
+ </div>
+ </div>
+ `;
+
+ return `<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+ <meta http-equiv="refresh" content="5">
+ <title>System Logs</title>
+ <style>
+ * { box-sizing: border-box; }
+ body { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #1e1e1e; color: #d4d4d4; padding: 16px; margin: 0; }
+ .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; }
+ @media (max-width: 400px) { body { padding: 10px; } .log-container { padding: 10px; font-size: 12px; } }
+ </style>
+</head>
+<body onload="window.scrollTo(0,document.body.scrollHeight);">
+ ${content}
+</body>
+</html>`;
}
module.exports = {