Merge branch 'dev'
CI/CD / deploy (push) Failing after 2m27s
Details
CI/CD / deploy (push) Failing after 2m27s
Details
This commit is contained in:
commit
019ceadd0d
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* Bug Report API — accepts user-submitted bug reports and emails them.
|
||||
*/
|
||||
|
||||
import { Hono } from "hono";
|
||||
import { getSmtpTransport } from "./notification-service";
|
||||
|
||||
export const bugReportRouter = new Hono();
|
||||
|
||||
const MAX_SCREENSHOT_BYTES = 3 * 1024 * 1024; // 3 MB
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
|
||||
bugReportRouter.post("/", async (c) => {
|
||||
let body: any;
|
||||
try {
|
||||
body = await c.req.json();
|
||||
} catch {
|
||||
return c.json({ error: "Invalid JSON" }, 400);
|
||||
}
|
||||
|
||||
const { userAgent, url, errors, comments, screenshot } = body as {
|
||||
userAgent?: string;
|
||||
url?: string;
|
||||
errors?: string[];
|
||||
comments?: string;
|
||||
screenshot?: string; // data URL
|
||||
};
|
||||
|
||||
if (!comments?.trim() && (!errors || errors.length === 0)) {
|
||||
return c.json({ error: "Please provide comments or error details" }, 400);
|
||||
}
|
||||
|
||||
// Screenshot handling
|
||||
let attachments: any[] = [];
|
||||
let screenshotCid = "";
|
||||
if (screenshot && typeof screenshot === "string" && screenshot.startsWith("data:image/")) {
|
||||
// Validate size (base64 is ~4/3 of raw, so check decoded)
|
||||
const base64Part = screenshot.split(",")[1];
|
||||
if (!base64Part) return c.json({ error: "Invalid screenshot data URL" }, 400);
|
||||
const rawBytes = Buffer.from(base64Part, "base64");
|
||||
if (rawBytes.length > MAX_SCREENSHOT_BYTES) {
|
||||
return c.json({ error: "Screenshot exceeds 3 MB limit" }, 413);
|
||||
}
|
||||
|
||||
// Determine extension from mime
|
||||
const mimeMatch = screenshot.match(/^data:image\/(png|jpeg|jpg|gif|webp);/);
|
||||
const ext = mimeMatch ? mimeMatch[1].replace("jpeg", "jpg") : "png";
|
||||
const filename = `bugreport-${Date.now()}.${ext}`;
|
||||
|
||||
// Save to generated files
|
||||
const savePath = `/data/files/generated/${filename}`;
|
||||
try {
|
||||
await Bun.write(savePath, rawBytes);
|
||||
} catch {
|
||||
// Non-fatal — still send email without saved file
|
||||
}
|
||||
|
||||
screenshotCid = `screenshot-${Date.now()}`;
|
||||
attachments = [{
|
||||
filename,
|
||||
content: rawBytes,
|
||||
cid: screenshotCid,
|
||||
contentType: `image/${ext === "jpg" ? "jpeg" : ext}`,
|
||||
}];
|
||||
}
|
||||
|
||||
// Build HTML email
|
||||
const errorList = (errors && errors.length > 0)
|
||||
? errors.map(e => `<li style="margin:4px 0;font-size:13px;color:#fca5a5;">${escapeHtml(e)}</li>`).join("")
|
||||
: "";
|
||||
|
||||
const html = `
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:600px;margin:0 auto;padding:24px;">
|
||||
<div style="background:#1e293b;border-radius:10px;padding:20px;color:#e2e8f0;">
|
||||
<h2 style="margin:0 0 12px;font-size:18px;color:#f87171;">Bug Report</h2>
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px;color:#94a3b8;">
|
||||
<tr><td style="padding:4px 8px 4px 0;font-weight:600;color:#cbd5e1;white-space:nowrap;">Page URL</td><td style="padding:4px 0;">${escapeHtml(url || "unknown")}</td></tr>
|
||||
<tr><td style="padding:4px 8px 4px 0;font-weight:600;color:#cbd5e1;white-space:nowrap;">Device</td><td style="padding:4px 0;">${escapeHtml(userAgent || "unknown")}</td></tr>
|
||||
</table>
|
||||
${errorList ? `<div style="margin-top:12px;"><strong style="color:#fca5a5;font-size:13px;">Recent Errors:</strong><ul style="margin:4px 0 0;padding-left:20px;">${errorList}</ul></div>` : ""}
|
||||
${comments?.trim() ? `<div style="margin-top:12px;"><strong style="color:#cbd5e1;font-size:13px;">Comments:</strong><p style="margin:4px 0 0;font-size:14px;color:#e2e8f0;white-space:pre-wrap;">${escapeHtml(comments)}</p></div>` : ""}
|
||||
${screenshotCid ? `<div style="margin-top:16px;"><strong style="color:#cbd5e1;font-size:13px;">Screenshot:</strong><br><img src="cid:${screenshotCid}" style="margin-top:8px;max-width:100%;border-radius:6px;border:1px solid #334155;"></div>` : ""}
|
||||
</div>
|
||||
<p style="margin:12px 0 0;font-size:11px;color:#64748b;text-align:center;">Sent from rSpace Bug Reporter</p>
|
||||
</div>`;
|
||||
|
||||
try {
|
||||
const transport = await getSmtpTransport();
|
||||
if (!transport) {
|
||||
console.error("[bug-report] No SMTP transport available");
|
||||
return c.json({ error: "Email service unavailable" }, 503);
|
||||
}
|
||||
|
||||
await transport.sendMail({
|
||||
from: "rSpace Bugs <bugs@rspace.online>",
|
||||
to: "jeff@jeffemmett.com",
|
||||
subject: `[Bug] ${url || "rSpace"} — ${(comments || "").slice(0, 60) || "Error report"}`,
|
||||
html,
|
||||
attachments,
|
||||
});
|
||||
|
||||
console.log(`[bug-report] Report sent for ${url}`);
|
||||
return c.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
console.error("[bug-report] Failed to send:", err.message);
|
||||
return c.json({ error: "Failed to send report" }, 500);
|
||||
}
|
||||
});
|
||||
|
|
@ -107,6 +107,7 @@ import { registerUserConnection, unregisterUserConnection, notify } from "./noti
|
|||
import { SystemClock } from "./clock-service";
|
||||
import type { ClockPayload } from "./clock-service";
|
||||
import { miRoutes } from "./mi-routes";
|
||||
import { bugReportRouter } from "./bug-report-routes";
|
||||
|
||||
// ── Process-level error safety net (prevent crash on unhandled socket errors) ──
|
||||
process.on('uncaughtException', (err) => {
|
||||
|
|
@ -524,6 +525,9 @@ app.route("/api/mi", miRoutes);
|
|||
app.route("/rtasks/check", checklistCheckRoutes);
|
||||
app.route("/api/rtasks", checklistApiRoutes);
|
||||
|
||||
// ── Bug Report API ──
|
||||
app.route("/api/bug-report", bugReportRouter);
|
||||
|
||||
// ── Magic Link Responses (top-level, bypasses space auth) ──
|
||||
app.route("/respond", magicLinkRoutes);
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const SMTP_PASS = process.env.SMTP_PASS || "";
|
|||
|
||||
let _smtpTransport: any = null;
|
||||
|
||||
async function getSmtpTransport() {
|
||||
export async function getSmtpTransport() {
|
||||
if (_smtpTransport) return _smtpTransport;
|
||||
const isInternal = SMTP_HOST.includes('mailcow') || SMTP_HOST.includes('postfix');
|
||||
if (!SMTP_PASS && !isInternal) return null;
|
||||
|
|
|
|||
153
server/shell.ts
153
server/shell.ts
|
|
@ -289,6 +289,7 @@ export function renderShell(opts: ShellOptions): string {
|
|||
<style>${INFO_PANEL_CSS}</style>
|
||||
<style>${SUBNAV_CSS}</style>
|
||||
<style>${TABBAR_CSS}</style>
|
||||
<style>${BUG_REPORT_CSS}</style>
|
||||
</head>
|
||||
<body data-space-visibility="${escapeAttr(spaceVisibility)}" data-space-slug="${escapeAttr(spaceSlug)}" data-module-id="${escapeAttr(moduleId)}" data-scope-overrides="${escapeAttr(JSON.stringify(scopeOverrides))}">
|
||||
<div class="rspace-banner rspace-banner--install" id="pwa-install-banner" style="display:none">
|
||||
|
|
@ -1566,6 +1567,93 @@ export function renderShell(opts: ShellOptions): string {
|
|||
})();
|
||||
}
|
||||
</script>
|
||||
${renderBugReportWidget()}
|
||||
<script>(function(){
|
||||
// ── Bug Report: error ring buffer ──
|
||||
var _bugErrors = [];
|
||||
window.addEventListener('error', function(e) {
|
||||
_bugErrors.push(e.message + (e.filename ? ' (' + e.filename + ':' + e.lineno + ')' : ''));
|
||||
if (_bugErrors.length > 5) _bugErrors.shift();
|
||||
});
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
_bugErrors.push('Promise: ' + (e.reason?.message || e.reason || 'unknown'));
|
||||
if (_bugErrors.length > 5) _bugErrors.shift();
|
||||
});
|
||||
|
||||
var btn = document.getElementById('rspace-bug-btn');
|
||||
var overlay = document.getElementById('rspace-bug-overlay');
|
||||
var modal = document.getElementById('rspace-bug-modal');
|
||||
var errField = document.getElementById('bug-errors');
|
||||
var deviceField = document.getElementById('bug-device');
|
||||
var commentsField = document.getElementById('bug-comments');
|
||||
var fileInput = document.getElementById('bug-screenshot');
|
||||
var submitBtn = document.getElementById('rspace-bug-submit');
|
||||
var statusEl = document.getElementById('rspace-bug-status');
|
||||
|
||||
function openModal() {
|
||||
errField.value = _bugErrors.length ? _bugErrors.join('\\n') : '';
|
||||
deviceField.value = navigator.userAgent;
|
||||
commentsField.value = '';
|
||||
fileInput.value = '';
|
||||
statusEl.textContent = '';
|
||||
submitBtn.disabled = false;
|
||||
overlay.classList.add('open');
|
||||
}
|
||||
function closeModal() { overlay.classList.remove('open'); }
|
||||
|
||||
btn.addEventListener('click', openModal);
|
||||
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeModal(); });
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && overlay.classList.contains('open')) closeModal();
|
||||
});
|
||||
|
||||
submitBtn.addEventListener('click', function() {
|
||||
var file = fileInput.files && fileInput.files[0];
|
||||
if (file && file.size > 3 * 1024 * 1024) {
|
||||
statusEl.textContent = 'Screenshot exceeds 3 MB limit';
|
||||
statusEl.style.color = 'var(--rs-error, #f87171)';
|
||||
return;
|
||||
}
|
||||
submitBtn.disabled = true;
|
||||
statusEl.textContent = 'Sending...';
|
||||
statusEl.style.color = 'var(--rs-text-muted, #94a3b8)';
|
||||
|
||||
function send(screenshotData) {
|
||||
fetch('/api/bug-report', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
userAgent: navigator.userAgent,
|
||||
url: location.href,
|
||||
errors: _bugErrors.slice(),
|
||||
comments: commentsField.value,
|
||||
screenshot: screenshotData || undefined,
|
||||
}),
|
||||
}).then(function(r) {
|
||||
if (r.ok) {
|
||||
statusEl.textContent = 'Report sent — thank you!';
|
||||
statusEl.style.color = 'var(--rs-primary, #14b8a6)';
|
||||
setTimeout(closeModal, 1500);
|
||||
} else {
|
||||
return r.json().then(function(d) { throw new Error(d.error || 'Failed'); });
|
||||
}
|
||||
}).catch(function(err) {
|
||||
statusEl.textContent = err.message || 'Failed to send';
|
||||
statusEl.style.color = 'var(--rs-error, #f87171)';
|
||||
submitBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
if (file) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function() { send(reader.result); };
|
||||
reader.onerror = function() { send(null); };
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
send(null);
|
||||
}
|
||||
});
|
||||
})()</script>
|
||||
${scripts}
|
||||
</body>
|
||||
</html>`);
|
||||
|
|
@ -2196,6 +2284,71 @@ const TABBAR_CSS = `
|
|||
}
|
||||
`;
|
||||
|
||||
// ── Bug report floating button + modal ──
|
||||
|
||||
const BUG_REPORT_CSS = `
|
||||
#rspace-bug-btn {
|
||||
position: fixed; bottom: 20px; right: 20px; z-index: 9990;
|
||||
width: 36px; height: 36px; border-radius: 50%;
|
||||
background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-border, #334155);
|
||||
color: var(--rs-text-muted, #94a3b8); cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0.45; transition: opacity 0.2s, transform 0.2s;
|
||||
padding: 0; box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
}
|
||||
#rspace-bug-btn:hover { opacity: 1; transform: scale(1.1); }
|
||||
#rspace-bug-btn svg { width: 18px; height: 18px; }
|
||||
html.rspace-embedded #rspace-bug-btn { display: none !important; }
|
||||
#rspace-bug-overlay {
|
||||
display: none; position: fixed; inset: 0; z-index: 10002;
|
||||
background: rgba(0,0,0,0.5); backdrop-filter: blur(4px);
|
||||
}
|
||||
#rspace-bug-overlay.open { display: flex; align-items: center; justify-content: center; }
|
||||
#rspace-bug-modal {
|
||||
z-index: 10003; width: 90vw; max-width: 480px; max-height: 85vh; overflow-y: auto;
|
||||
background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-border, #334155);
|
||||
border-radius: 12px; padding: 20px; color: var(--rs-text, #e2e8f0);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
#rspace-bug-modal h3 { margin: 0 0 16px; font-size: 16px; color: var(--rs-error, #f87171); }
|
||||
#rspace-bug-modal label { display: block; font-size: 12px; font-weight: 600; color: var(--rs-text-muted, #94a3b8); margin: 10px 0 4px; }
|
||||
#rspace-bug-modal textarea,
|
||||
#rspace-bug-modal input[type="text"] {
|
||||
width: 100%; box-sizing: border-box; padding: 8px; font-size: 13px;
|
||||
background: var(--rs-bg, #0f172a); color: var(--rs-text, #e2e8f0);
|
||||
border: 1px solid var(--rs-border, #334155); border-radius: 6px; resize: vertical;
|
||||
}
|
||||
#rspace-bug-modal input[type="file"] { font-size: 12px; color: var(--rs-text-muted, #94a3b8); margin-top: 2px; }
|
||||
#rspace-bug-submit {
|
||||
margin-top: 14px; width: 100%; padding: 10px; font-size: 14px; font-weight: 600;
|
||||
background: var(--rs-primary, #14b8a6); color: #fff; border: none; border-radius: 6px; cursor: pointer;
|
||||
}
|
||||
#rspace-bug-submit:hover { filter: brightness(1.1); }
|
||||
#rspace-bug-submit:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
#rspace-bug-status { margin-top: 8px; font-size: 12px; text-align: center; min-height: 1.2em; }
|
||||
`;
|
||||
|
||||
function renderBugReportWidget(): string {
|
||||
return `<button id="rspace-bug-btn" title="Report a bug" aria-label="Report a bug">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2l1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>
|
||||
</button>
|
||||
<div id="rspace-bug-overlay">
|
||||
<div id="rspace-bug-modal">
|
||||
<h3>Report a Bug</h3>
|
||||
<label for="bug-errors">Recent errors</label>
|
||||
<textarea id="bug-errors" rows="3" readonly placeholder="No errors captured"></textarea>
|
||||
<label for="bug-device">Device info</label>
|
||||
<input type="text" id="bug-device" readonly>
|
||||
<label for="bug-comments">What happened?</label>
|
||||
<textarea id="bug-comments" rows="4" placeholder="Describe what you were doing when the issue occurred..."></textarea>
|
||||
<label for="bug-screenshot">Screenshot (optional, max 3 MB)</label>
|
||||
<input type="file" id="bug-screenshot" accept="image/*">
|
||||
<button id="rspace-bug-submit">Send Report</button>
|
||||
<div id="rspace-bug-status"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderTabBar(tabs: Array<{ id: string; label: string; icon?: string }>, activeTab: string | undefined, basePath: string): string {
|
||||
if (tabs.length === 0) return '';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue