@1agh/maude 0.50.0 → 0.51.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/studio/client/app.jsx +25 -0
- package/apps/studio/client/github.js +2 -0
- package/apps/studio/client/panels/CloudBar.jsx +119 -15
- package/apps/studio/client/report-bug.jsx +590 -0
- package/apps/studio/client/styles/3-shell-maude.css +2 -0
- package/apps/studio/client/styles/4-components.css +84 -0
- package/apps/studio/cloud/endpoints.ts +86 -38
- package/apps/studio/debug-bundle.ts +144 -0
- package/apps/studio/dist/client.bundle.js +902 -897
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/git/service.ts +1 -1
- package/apps/studio/http.ts +92 -1
- package/apps/studio/server.ts +7 -0
- package/apps/studio/sync/workspace-signin.ts +15 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +8 -0
- package/apps/studio/test/cloud-endpoints.test.ts +130 -0
- package/apps/studio/test/debug-bundle.test.ts +105 -0
- package/apps/studio/test/workspace-signin.test.ts +19 -0
- package/apps/studio/whats-new.json +9 -0
- package/cli/lib/gitignore-block.mjs +1 -0
- package/package.json +9 -8
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
// client/report-bug.jsx — the Report-a-Bug dialog (feature-bug-report-button).
|
|
2
|
+
//
|
|
3
|
+
// Consent-first by construction (docs/report-schema.md): the dialog's middle
|
|
4
|
+
// step IS the payload preview — every optional item (screenshot, log tail,
|
|
5
|
+
// project name, active canvas, crash logs) has its own checkbox, description
|
|
6
|
+
// is the only mandatory field, and nothing transmits until "Send report".
|
|
7
|
+
// Screenshots can be redacted in place (black-box rects, flattened into the
|
|
8
|
+
// PNG client-side — the original pixels never leave the machine).
|
|
9
|
+
//
|
|
10
|
+
// Transport: POST /_api/report (dev-server proxy → cloud intake). Cloud down
|
|
11
|
+
// or declined → POST /_api/report-fallback writes the bundle under
|
|
12
|
+
// `<designRoot>/_reports/` and we open a prefilled public-issue URL (text
|
|
13
|
+
// only — media never goes to the public repo).
|
|
14
|
+
|
|
15
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
16
|
+
|
|
17
|
+
import { invoke, isNativeApp, openGitHubUrl } from './github.js';
|
|
18
|
+
|
|
19
|
+
const MAX_SHOTS = 3;
|
|
20
|
+
const PUBLIC_ISSUES_URL = 'https://github.com/1aGh/maude/issues/new';
|
|
21
|
+
|
|
22
|
+
function randHex(n) {
|
|
23
|
+
const bytes = new Uint8Array(n / 2);
|
|
24
|
+
crypto.getRandomValues(bytes);
|
|
25
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function installId() {
|
|
29
|
+
try {
|
|
30
|
+
let id = localStorage.getItem('maude-install-id');
|
|
31
|
+
if (!id) {
|
|
32
|
+
id = `i-${randHex(12)}`;
|
|
33
|
+
localStorage.setItem('maude-install-id', id);
|
|
34
|
+
}
|
|
35
|
+
return id;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Flatten one screenshot: source image + redaction rects → PNG blob. */
|
|
42
|
+
async function flattenShot(shot) {
|
|
43
|
+
const img = new Image();
|
|
44
|
+
img.src = shot.url;
|
|
45
|
+
await img.decode();
|
|
46
|
+
const canvas = document.createElement('canvas');
|
|
47
|
+
canvas.width = img.naturalWidth;
|
|
48
|
+
canvas.height = img.naturalHeight;
|
|
49
|
+
const c2d = canvas.getContext('2d');
|
|
50
|
+
c2d.drawImage(img, 0, 0);
|
|
51
|
+
c2d.fillStyle = '#000';
|
|
52
|
+
for (const r of shot.rects) {
|
|
53
|
+
c2d.fillRect(
|
|
54
|
+
r.x * img.naturalWidth,
|
|
55
|
+
r.y * img.naturalHeight,
|
|
56
|
+
r.w * img.naturalWidth,
|
|
57
|
+
r.h * img.naturalHeight
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const blobToDataUrl = (blob) =>
|
|
64
|
+
new Promise((resolve) => {
|
|
65
|
+
const fr = new FileReader();
|
|
66
|
+
fr.onload = () => resolve(fr.result);
|
|
67
|
+
fr.readAsDataURL(blob);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** One screenshot preview tile: redact-mode drag draws normalized black rects. */
|
|
71
|
+
function ShotTile({ shot, redacting, onRects, onRemove }) {
|
|
72
|
+
const boxRef = useRef(null);
|
|
73
|
+
const dragRef = useRef(null);
|
|
74
|
+
const [draft, setDraft] = useState(null);
|
|
75
|
+
|
|
76
|
+
const norm = (e) => {
|
|
77
|
+
const b = boxRef.current.getBoundingClientRect();
|
|
78
|
+
return {
|
|
79
|
+
x: Math.min(Math.max((e.clientX - b.left) / b.width, 0), 1),
|
|
80
|
+
y: Math.min(Math.max((e.clientY - b.top) / b.height, 0), 1),
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="rb-shot" data-testid="report-bug-shot">
|
|
86
|
+
<div
|
|
87
|
+
ref={boxRef}
|
|
88
|
+
className={`rb-shot-box${redacting ? ' is-redacting' : ''}`}
|
|
89
|
+
onPointerDown={(e) => {
|
|
90
|
+
if (!redacting) return;
|
|
91
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
92
|
+
dragRef.current = norm(e);
|
|
93
|
+
setDraft({ ...dragRef.current, w: 0, h: 0 });
|
|
94
|
+
}}
|
|
95
|
+
onPointerMove={(e) => {
|
|
96
|
+
if (!redacting || !dragRef.current) return;
|
|
97
|
+
const p = norm(e);
|
|
98
|
+
const s = dragRef.current;
|
|
99
|
+
setDraft({
|
|
100
|
+
x: Math.min(s.x, p.x),
|
|
101
|
+
y: Math.min(s.y, p.y),
|
|
102
|
+
w: Math.abs(p.x - s.x),
|
|
103
|
+
h: Math.abs(p.y - s.y),
|
|
104
|
+
});
|
|
105
|
+
}}
|
|
106
|
+
onPointerUp={() => {
|
|
107
|
+
if (!redacting || !dragRef.current) return;
|
|
108
|
+
dragRef.current = null;
|
|
109
|
+
if (draft && draft.w > 0.005 && draft.h > 0.005) onRects([...shot.rects, draft]);
|
|
110
|
+
setDraft(null);
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<img src={shot.url} alt="screenshot to attach" draggable={false} />
|
|
114
|
+
{[...shot.rects, ...(draft ? [draft] : [])].map((r, i) => (
|
|
115
|
+
<div
|
|
116
|
+
key={i}
|
|
117
|
+
className="rb-redact-rect"
|
|
118
|
+
style={{
|
|
119
|
+
left: `${r.x * 100}%`,
|
|
120
|
+
top: `${r.y * 100}%`,
|
|
121
|
+
width: `${r.w * 100}%`,
|
|
122
|
+
height: `${r.h * 100}%`,
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
<div className="rb-shot-actions">
|
|
128
|
+
{shot.rects.length > 0 && (
|
|
129
|
+
<button type="button" className="rb-link" onClick={() => onRects([])}>
|
|
130
|
+
Clear redactions
|
|
131
|
+
</button>
|
|
132
|
+
)}
|
|
133
|
+
<button type="button" className="rb-link" data-testid="report-bug-shot-remove" onClick={onRemove}>
|
|
134
|
+
Remove
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function ReportBugDialog({ open, onClose }) {
|
|
142
|
+
const [step, setStep] = useState('describe'); // describe | preview | sending | done | fallback
|
|
143
|
+
const [description, setDescription] = useState('');
|
|
144
|
+
const [bundle, setBundle] = useState(null);
|
|
145
|
+
const [shots, setShots] = useState([]); // {url, blob, rects:[{x,y,w,h} normalized]}
|
|
146
|
+
const [crashLogs, setCrashLogs] = useState([]); // native only: {name, firstLine}
|
|
147
|
+
const [consent, setConsent] = useState({
|
|
148
|
+
logTail: true,
|
|
149
|
+
activeCanvas: true,
|
|
150
|
+
projectName: false,
|
|
151
|
+
crashLogs: true,
|
|
152
|
+
});
|
|
153
|
+
const [redacting, setRedacting] = useState(false);
|
|
154
|
+
const [result, setResult] = useState(null); // {issueUrl, issueNumber} | {fallbackDir}
|
|
155
|
+
const [error, setError] = useState(null);
|
|
156
|
+
|
|
157
|
+
const reset = useCallback(() => {
|
|
158
|
+
for (const s of shots) URL.revokeObjectURL(s.url);
|
|
159
|
+
setStep('describe');
|
|
160
|
+
setDescription('');
|
|
161
|
+
setBundle(null);
|
|
162
|
+
setShots([]);
|
|
163
|
+
setCrashLogs([]);
|
|
164
|
+
setRedacting(false);
|
|
165
|
+
setResult(null);
|
|
166
|
+
setError(null);
|
|
167
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
168
|
+
}, [shots]);
|
|
169
|
+
|
|
170
|
+
const addShotBlob = useCallback((blob) => {
|
|
171
|
+
if (!blob) return;
|
|
172
|
+
setShots((prev) =>
|
|
173
|
+
prev.length >= MAX_SHOTS
|
|
174
|
+
? prev
|
|
175
|
+
: [...prev, { url: URL.createObjectURL(blob), blob, rects: [] }]
|
|
176
|
+
);
|
|
177
|
+
}, []);
|
|
178
|
+
|
|
179
|
+
// On open: pull the scrubbed bundle, capture the active artboard, list
|
|
180
|
+
// native crash logs. Every leg fails soft — a report with fewer attachments
|
|
181
|
+
// beats no dialog.
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (!open) return;
|
|
184
|
+
let alive = true;
|
|
185
|
+
fetch('/_api/debug-bundle')
|
|
186
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
187
|
+
.then((b) => alive && setBundle(b))
|
|
188
|
+
.catch(() => {});
|
|
189
|
+
fetch('/_api/export', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: JSON.stringify({ format: 'png', scope: 'artboard' }),
|
|
193
|
+
})
|
|
194
|
+
.then((r) => (r.ok ? r.blob() : null))
|
|
195
|
+
.then((blob) => alive && blob && addShotBlob(blob))
|
|
196
|
+
.catch(() => {});
|
|
197
|
+
if (isNativeApp()) {
|
|
198
|
+
invoke('list_crash_logs')
|
|
199
|
+
.then((logs) => alive && setCrashLogs(Array.isArray(logs) ? logs.slice(0, 3) : []))
|
|
200
|
+
.catch(() => {});
|
|
201
|
+
}
|
|
202
|
+
return () => {
|
|
203
|
+
alive = false;
|
|
204
|
+
};
|
|
205
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
206
|
+
}, [open]);
|
|
207
|
+
|
|
208
|
+
// Paste-to-attach — the lane that covers native-shell bugs the canvas
|
|
209
|
+
// capture can't see (user screenshots the window, pastes here).
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!open) return;
|
|
212
|
+
function onPaste(e) {
|
|
213
|
+
for (const item of e.clipboardData?.items ?? []) {
|
|
214
|
+
if (item.type.startsWith('image/')) addShotBlob(item.getAsFile());
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
window.addEventListener('paste', onPaste);
|
|
218
|
+
return () => window.removeEventListener('paste', onPaste);
|
|
219
|
+
}, [open, addShotBlob]);
|
|
220
|
+
|
|
221
|
+
if (!open) return null;
|
|
222
|
+
|
|
223
|
+
const close = () => {
|
|
224
|
+
reset();
|
|
225
|
+
onClose();
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
async function buildReport() {
|
|
229
|
+
const report = {
|
|
230
|
+
schema: 'maude-report/v1',
|
|
231
|
+
reportId: `r-${randHex(8)}`,
|
|
232
|
+
createdAt: new Date().toISOString(),
|
|
233
|
+
app: {
|
|
234
|
+
maudeVersion: bundle?.app?.maudeVersion ?? 'unknown',
|
|
235
|
+
surface: isNativeApp() ? 'native' : 'browser',
|
|
236
|
+
platform: bundle?.app?.platform ?? 'unknown',
|
|
237
|
+
arch: bundle?.app?.arch ?? 'unknown',
|
|
238
|
+
},
|
|
239
|
+
context: {
|
|
240
|
+
projectName: consent.projectName ? (bundle?.context?.projectName ?? null) : null,
|
|
241
|
+
activeCanvas: consent.activeCanvas ? (bundle?.context?.activeCanvas ?? null) : null,
|
|
242
|
+
},
|
|
243
|
+
description: description.trim(),
|
|
244
|
+
attachments: {
|
|
245
|
+
screenshots: shots.length,
|
|
246
|
+
serverLogTail: consent.logTail && !!bundle?.logs?.serverLogTail,
|
|
247
|
+
crashLogs: consent.crashLogs ? crashLogs.length : 0,
|
|
248
|
+
},
|
|
249
|
+
logs: {},
|
|
250
|
+
};
|
|
251
|
+
const id = installId();
|
|
252
|
+
if (id) report.installId = id;
|
|
253
|
+
if (consent.logTail && bundle?.logs?.serverLogTail) {
|
|
254
|
+
report.logs.serverLogTail = bundle.logs.serverLogTail;
|
|
255
|
+
}
|
|
256
|
+
if (consent.crashLogs && crashLogs.length && isNativeApp()) {
|
|
257
|
+
report.logs.crashLogs = [];
|
|
258
|
+
for (const log of crashLogs) {
|
|
259
|
+
try {
|
|
260
|
+
const body = await invoke('read_crash_log', { name: log.name });
|
|
261
|
+
report.logs.crashLogs.push({ name: log.name, body });
|
|
262
|
+
} catch {
|
|
263
|
+
/* an unreadable crash log is dropped, not fatal */
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return report;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function send() {
|
|
271
|
+
setStep('sending');
|
|
272
|
+
setError(null);
|
|
273
|
+
try {
|
|
274
|
+
const report = await buildReport();
|
|
275
|
+
const form = new FormData();
|
|
276
|
+
form.set('report', JSON.stringify(report));
|
|
277
|
+
for (const shot of shots) {
|
|
278
|
+
form.append('screenshot', await flattenShot(shot), 'screenshot.png');
|
|
279
|
+
}
|
|
280
|
+
const res = await fetch('/_api/report', { method: 'POST', body: form });
|
|
281
|
+
const json = await res.json().catch(() => null);
|
|
282
|
+
if (res.ok && json?.ok) {
|
|
283
|
+
setResult(json);
|
|
284
|
+
setStep('done');
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
setError(json?.error ?? `report failed (HTTP ${res.status})`);
|
|
288
|
+
setStep('fallback');
|
|
289
|
+
} catch {
|
|
290
|
+
setError('Maude could not reach the report service.');
|
|
291
|
+
setStep('fallback');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function saveFallback() {
|
|
296
|
+
try {
|
|
297
|
+
const report = await buildReport();
|
|
298
|
+
const screenshots = [];
|
|
299
|
+
for (const shot of shots) screenshots.push(await blobToDataUrl(await flattenShot(shot)));
|
|
300
|
+
const res = await fetch('/_api/report-fallback', {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: { 'Content-Type': 'application/json' },
|
|
303
|
+
body: JSON.stringify({ report, screenshots }),
|
|
304
|
+
});
|
|
305
|
+
const json = await res.json().catch(() => null);
|
|
306
|
+
const title = encodeURIComponent(report.description.split('\n')[0].slice(0, 80));
|
|
307
|
+
const body = encodeURIComponent(
|
|
308
|
+
`${report.description}\n\n> Maude ${report.app.maudeVersion} · ${report.app.platform} ${report.app.arch} · ${report.app.surface}\n> Local report bundle: \`${json?.dir ?? '.design/_reports/'}\` (attach its files here)`
|
|
309
|
+
);
|
|
310
|
+
const url = `${PUBLIC_ISSUES_URL}?title=${title}&body=${body}&labels=report`;
|
|
311
|
+
if (isNativeApp()) {
|
|
312
|
+
try {
|
|
313
|
+
await openGitHubUrl(url);
|
|
314
|
+
} catch {
|
|
315
|
+
window.open(url, '_blank', 'noopener');
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
window.open(url, '_blank', 'noopener');
|
|
319
|
+
}
|
|
320
|
+
setResult({ fallbackDir: json?.dir ?? null });
|
|
321
|
+
setStep('done');
|
|
322
|
+
} catch {
|
|
323
|
+
setError('Saving the local bundle failed too — please file the issue by hand.');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const canPreview = description.trim().length > 0;
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div className="help-modal-backdrop" onClick={close} data-testid="report-bug-dialog">
|
|
331
|
+
<div
|
|
332
|
+
className="help-modal rb-modal"
|
|
333
|
+
role="dialog"
|
|
334
|
+
aria-modal="true"
|
|
335
|
+
aria-labelledby="rb-title"
|
|
336
|
+
onClick={(e) => e.stopPropagation()}
|
|
337
|
+
>
|
|
338
|
+
<header className="help-modal-hd">
|
|
339
|
+
<span className="title" id="rb-title">
|
|
340
|
+
Report a bug
|
|
341
|
+
</span>
|
|
342
|
+
<button type="button" className="help-modal-close" onClick={close} aria-label="Close">
|
|
343
|
+
×
|
|
344
|
+
</button>
|
|
345
|
+
</header>
|
|
346
|
+
|
|
347
|
+
<div className="help-modal-body rb-body">
|
|
348
|
+
{step === 'describe' && (
|
|
349
|
+
<>
|
|
350
|
+
<label className="rb-label" htmlFor="rb-desc">
|
|
351
|
+
What happened? What did you expect instead?
|
|
352
|
+
</label>
|
|
353
|
+
<textarea
|
|
354
|
+
id="rb-desc"
|
|
355
|
+
className="rb-desc"
|
|
356
|
+
data-testid="report-bug-description"
|
|
357
|
+
rows={5}
|
|
358
|
+
autoFocus
|
|
359
|
+
value={description}
|
|
360
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
361
|
+
placeholder="e.g. Clicking Export → PNG shows a spinner forever…"
|
|
362
|
+
/>
|
|
363
|
+
<p className="rb-hint">
|
|
364
|
+
Next you'll see exactly what gets attached — nothing is sent until you approve it.
|
|
365
|
+
</p>
|
|
366
|
+
<footer className="rb-actions">
|
|
367
|
+
<button type="button" className="rb-btn" onClick={close}>
|
|
368
|
+
Cancel
|
|
369
|
+
</button>
|
|
370
|
+
<button
|
|
371
|
+
type="button"
|
|
372
|
+
className="rb-btn rb-primary"
|
|
373
|
+
data-testid="report-bug-preview"
|
|
374
|
+
disabled={!canPreview}
|
|
375
|
+
onClick={() => setStep('preview')}
|
|
376
|
+
>
|
|
377
|
+
Review & attach
|
|
378
|
+
</button>
|
|
379
|
+
</footer>
|
|
380
|
+
</>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{step === 'preview' && (
|
|
384
|
+
<>
|
|
385
|
+
<div className="rb-section">
|
|
386
|
+
<div className="rb-section-hd">
|
|
387
|
+
<span>Screenshots ({shots.length})</span>
|
|
388
|
+
{shots.length > 0 && (
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
className={`rb-link${redacting ? ' is-on' : ''}`}
|
|
392
|
+
data-testid="report-bug-redact"
|
|
393
|
+
onClick={() => setRedacting((v) => !v)}
|
|
394
|
+
>
|
|
395
|
+
{redacting ? 'Done redacting' : 'Redact…'}
|
|
396
|
+
</button>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
{shots.length === 0 ? (
|
|
400
|
+
<p className="rb-hint">
|
|
401
|
+
No screenshot attached. Paste one (⌘V) to add it — up to {MAX_SHOTS}.
|
|
402
|
+
</p>
|
|
403
|
+
) : (
|
|
404
|
+
<>
|
|
405
|
+
{redacting && (
|
|
406
|
+
<p className="rb-hint">Drag over anything private — it ships as solid black.</p>
|
|
407
|
+
)}
|
|
408
|
+
<div className="rb-shots">
|
|
409
|
+
{shots.map((shot, i) => (
|
|
410
|
+
<ShotTile
|
|
411
|
+
key={shot.url}
|
|
412
|
+
shot={shot}
|
|
413
|
+
redacting={redacting}
|
|
414
|
+
onRects={(rects) =>
|
|
415
|
+
setShots((prev) => prev.map((s, j) => (j === i ? { ...s, rects } : s)))
|
|
416
|
+
}
|
|
417
|
+
onRemove={() => {
|
|
418
|
+
URL.revokeObjectURL(shot.url);
|
|
419
|
+
setShots((prev) => prev.filter((_, j) => j !== i));
|
|
420
|
+
}}
|
|
421
|
+
/>
|
|
422
|
+
))}
|
|
423
|
+
</div>
|
|
424
|
+
{shots.length < MAX_SHOTS && (
|
|
425
|
+
<p className="rb-hint">Paste (⌘V) to attach another screenshot.</p>
|
|
426
|
+
)}
|
|
427
|
+
</>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div className="rb-section">
|
|
432
|
+
<div className="rb-section-hd">
|
|
433
|
+
<span>Included with the report</span>
|
|
434
|
+
</div>
|
|
435
|
+
<label className="rb-check">
|
|
436
|
+
<input type="checkbox" checked disabled />
|
|
437
|
+
<span>
|
|
438
|
+
Maude {bundle?.app?.maudeVersion ?? '…'} · {bundle?.app?.platform ?? '…'}{' '}
|
|
439
|
+
{bundle?.app?.arch ?? ''} · {isNativeApp() ? 'desktop app' : 'browser'}
|
|
440
|
+
</span>
|
|
441
|
+
</label>
|
|
442
|
+
{bundle?.context?.activeCanvas && (
|
|
443
|
+
<label className="rb-check">
|
|
444
|
+
<input
|
|
445
|
+
type="checkbox"
|
|
446
|
+
checked={consent.activeCanvas}
|
|
447
|
+
onChange={(e) => setConsent({ ...consent, activeCanvas: e.target.checked })}
|
|
448
|
+
/>
|
|
449
|
+
<span>
|
|
450
|
+
Active canvas path — <code>{bundle.context.activeCanvas}</code>
|
|
451
|
+
</span>
|
|
452
|
+
</label>
|
|
453
|
+
)}
|
|
454
|
+
{bundle?.context?.projectName && (
|
|
455
|
+
<label className="rb-check">
|
|
456
|
+
<input
|
|
457
|
+
type="checkbox"
|
|
458
|
+
checked={consent.projectName}
|
|
459
|
+
onChange={(e) => setConsent({ ...consent, projectName: e.target.checked })}
|
|
460
|
+
/>
|
|
461
|
+
<span>
|
|
462
|
+
Project name — <code>{bundle.context.projectName}</code>
|
|
463
|
+
</span>
|
|
464
|
+
</label>
|
|
465
|
+
)}
|
|
466
|
+
{bundle?.logs?.serverLogTail ? (
|
|
467
|
+
<label className="rb-check">
|
|
468
|
+
<input
|
|
469
|
+
type="checkbox"
|
|
470
|
+
data-testid="report-bug-consent-logs"
|
|
471
|
+
checked={consent.logTail}
|
|
472
|
+
onChange={(e) => setConsent({ ...consent, logTail: e.target.checked })}
|
|
473
|
+
/>
|
|
474
|
+
<span>
|
|
475
|
+
Recent server log ({bundle.logs.serverLogTail.split('\n').length} lines,
|
|
476
|
+
paths & secrets scrubbed) —{' '}
|
|
477
|
+
<details className="rb-details">
|
|
478
|
+
<summary>view</summary>
|
|
479
|
+
<pre className="rb-log">{bundle.logs.serverLogTail}</pre>
|
|
480
|
+
</details>
|
|
481
|
+
</span>
|
|
482
|
+
</label>
|
|
483
|
+
) : null}
|
|
484
|
+
{isNativeApp() && crashLogs.length > 0 && (
|
|
485
|
+
<label className="rb-check">
|
|
486
|
+
<input
|
|
487
|
+
type="checkbox"
|
|
488
|
+
checked={consent.crashLogs}
|
|
489
|
+
onChange={(e) => setConsent({ ...consent, crashLogs: e.target.checked })}
|
|
490
|
+
/>
|
|
491
|
+
<span>
|
|
492
|
+
Crash logs ({crashLogs.length}) — <code>{crashLogs.map((l) => l.name).join(', ')}</code>
|
|
493
|
+
</span>
|
|
494
|
+
</label>
|
|
495
|
+
)}
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
<p className="rb-hint">
|
|
499
|
+
Your description and the system info above become a{' '}
|
|
500
|
+
<strong>public GitHub issue</strong>; screenshots and logs go to a private
|
|
501
|
+
repository only maintainers can open.
|
|
502
|
+
</p>
|
|
503
|
+
<footer className="rb-actions">
|
|
504
|
+
<button type="button" className="rb-btn" onClick={() => setStep('describe')}>
|
|
505
|
+
Back
|
|
506
|
+
</button>
|
|
507
|
+
<button
|
|
508
|
+
type="button"
|
|
509
|
+
className="rb-btn rb-primary"
|
|
510
|
+
data-testid="report-bug-send"
|
|
511
|
+
onClick={send}
|
|
512
|
+
>
|
|
513
|
+
Send report
|
|
514
|
+
</button>
|
|
515
|
+
</footer>
|
|
516
|
+
</>
|
|
517
|
+
)}
|
|
518
|
+
|
|
519
|
+
{step === 'sending' && <p className="rb-hint">Sending your report…</p>}
|
|
520
|
+
|
|
521
|
+
{step === 'done' && (
|
|
522
|
+
<>
|
|
523
|
+
{result?.issueUrl ? (
|
|
524
|
+
<p>
|
|
525
|
+
Thank you — your report is filed as{' '}
|
|
526
|
+
<button
|
|
527
|
+
type="button"
|
|
528
|
+
className="rb-link"
|
|
529
|
+
data-testid="report-bug-issue-link"
|
|
530
|
+
onClick={() => {
|
|
531
|
+
if (isNativeApp()) {
|
|
532
|
+
openGitHubUrl(result.issueUrl).catch(() =>
|
|
533
|
+
navigator.clipboard?.writeText(result.issueUrl)
|
|
534
|
+
);
|
|
535
|
+
} else {
|
|
536
|
+
window.open(result.issueUrl, '_blank', 'noopener');
|
|
537
|
+
}
|
|
538
|
+
}}
|
|
539
|
+
>
|
|
540
|
+
report #{result.issueNumber}
|
|
541
|
+
</button>
|
|
542
|
+
{' '}
|
|
543
|
+
on the public Maude issue tracker. Screenshots and logs stay in a private
|
|
544
|
+
repository only maintainers can open — the issue links to them.
|
|
545
|
+
</p>
|
|
546
|
+
) : (
|
|
547
|
+
<p>
|
|
548
|
+
The bundle is saved locally
|
|
549
|
+
{result?.fallbackDir ? (
|
|
550
|
+
<>
|
|
551
|
+
{' '}
|
|
552
|
+
at <code>{result.fallbackDir}</code>
|
|
553
|
+
</>
|
|
554
|
+
) : null}{' '}
|
|
555
|
+
and a prefilled GitHub issue was opened — attach the bundle files there.
|
|
556
|
+
</p>
|
|
557
|
+
)}
|
|
558
|
+
<footer className="rb-actions">
|
|
559
|
+
<button type="button" className="rb-btn rb-primary" onClick={close}>
|
|
560
|
+
Done
|
|
561
|
+
</button>
|
|
562
|
+
</footer>
|
|
563
|
+
</>
|
|
564
|
+
)}
|
|
565
|
+
|
|
566
|
+
{step === 'fallback' && (
|
|
567
|
+
<>
|
|
568
|
+
<p className="rb-error" data-testid="report-bug-error">
|
|
569
|
+
{error}
|
|
570
|
+
</p>
|
|
571
|
+
<p className="rb-hint">
|
|
572
|
+
You can save the report locally instead and file it on GitHub yourself — the
|
|
573
|
+
prefilled issue carries the text; screenshots stay on this machine unless you
|
|
574
|
+
attach them by hand.
|
|
575
|
+
</p>
|
|
576
|
+
<footer className="rb-actions">
|
|
577
|
+
<button type="button" className="rb-btn" onClick={() => setStep('preview')}>
|
|
578
|
+
Back
|
|
579
|
+
</button>
|
|
580
|
+
<button type="button" className="rb-btn rb-primary" onClick={saveFallback}>
|
|
581
|
+
Save locally & open GitHub
|
|
582
|
+
</button>
|
|
583
|
+
</footer>
|
|
584
|
+
</>
|
|
585
|
+
)}
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
);
|
|
590
|
+
}
|
|
@@ -1993,6 +1993,8 @@ body.st-scrubbing, body.st-scrubbing * { cursor: ew-resize !important; user-sele
|
|
|
1993
1993
|
.gi-rail { position: relative; flex: 0 0 auto; margin-top: auto; padding: var(--space-2) var(--space-3); border-top: 1px solid var(--border-default); display: flex; flex-direction: column; gap: var(--space-2); }
|
|
1994
1994
|
.gi-rail-hint { font-size: var(--type-xs); color: var(--fg-2); padding: var(--space-1) var(--space-1); }
|
|
1995
1995
|
.gi-rail-signin { width: 100%; justify-content: center; }
|
|
1996
|
+
.gi-rail-confirm { display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; padding: var(--space-2); border: 1px solid var(--border-default); border-radius: var(--radius-md); background: var(--bg-2); }
|
|
1997
|
+
.gi-rail-confirm .gi-rail-hint { flex: 1 1 100%; padding: 0; }
|
|
1996
1998
|
.gi-rail-err { font-size: var(--type-xs); color: var(--status-error); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1997
1999
|
.gi-avatar { border-radius: var(--radius-pill); display: grid; place-items: center; font-family: var(--font-mono); font-size: 10px; font-weight: 600; color: var(--accent-fg); background: var(--accent); flex: 0 0 auto; }
|
|
1998
2000
|
.gi-rail-account { display: flex; align-items: center; gap: var(--space-2); width: 100%; padding: var(--space-1) var(--space-2); border: 1px solid transparent; border-radius: var(--radius-md); background: transparent; color: var(--fg-0); cursor: pointer; text-align: left; }
|
|
@@ -1875,3 +1875,87 @@
|
|
|
1875
1875
|
flex-direction: column;
|
|
1876
1876
|
gap: 6px;
|
|
1877
1877
|
}
|
|
1878
|
+
|
|
1879
|
+
/* ----- Report-a-Bug dialog (feature-bug-report-button) -----
|
|
1880
|
+
Reuses the help-modal shell; rb-* covers the consent checklist, screenshot
|
|
1881
|
+
tiles, and the redaction overlay. Same DS voice: hard edges, mono, tokens. */
|
|
1882
|
+
.rb-modal { width: min(640px, 100%); }
|
|
1883
|
+
.rb-body { display: flex; flex-direction: column; gap: var(--u-s-3); }
|
|
1884
|
+
.rb-label { font-weight: 700; color: var(--u-fg-0); }
|
|
1885
|
+
.rb-desc {
|
|
1886
|
+
width: 100%;
|
|
1887
|
+
resize: vertical;
|
|
1888
|
+
background: var(--u-bg-0);
|
|
1889
|
+
border: 1px solid var(--u-border);
|
|
1890
|
+
color: var(--u-fg-0);
|
|
1891
|
+
font-family: var(--u-font-mono);
|
|
1892
|
+
font-size: var(--type-xs);
|
|
1893
|
+
padding: var(--u-s-2);
|
|
1894
|
+
}
|
|
1895
|
+
.rb-desc:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
|
|
1896
|
+
.rb-hint { color: var(--u-fg-2); }
|
|
1897
|
+
.rb-error { color: var(--u-danger, #c0392b); }
|
|
1898
|
+
.rb-section { display: flex; flex-direction: column; gap: var(--u-s-1); }
|
|
1899
|
+
.rb-section-hd {
|
|
1900
|
+
display: flex;
|
|
1901
|
+
align-items: center;
|
|
1902
|
+
justify-content: space-between;
|
|
1903
|
+
font-weight: 700;
|
|
1904
|
+
color: var(--u-fg-0);
|
|
1905
|
+
text-transform: uppercase;
|
|
1906
|
+
letter-spacing: var(--tracking-sku);
|
|
1907
|
+
font-size: 10px;
|
|
1908
|
+
}
|
|
1909
|
+
.rb-check { display: flex; gap: var(--u-s-2); align-items: baseline; cursor: pointer; }
|
|
1910
|
+
.rb-check input { accent-color: var(--u-accent, currentColor); }
|
|
1911
|
+
.rb-check code { color: var(--u-fg-0); }
|
|
1912
|
+
.rb-details summary { cursor: pointer; color: var(--u-fg-2); }
|
|
1913
|
+
.rb-log {
|
|
1914
|
+
max-height: 160px;
|
|
1915
|
+
overflow: auto;
|
|
1916
|
+
background: var(--u-bg-0);
|
|
1917
|
+
border: 1px solid var(--u-border);
|
|
1918
|
+
padding: var(--u-s-2);
|
|
1919
|
+
white-space: pre-wrap;
|
|
1920
|
+
word-break: break-all;
|
|
1921
|
+
}
|
|
1922
|
+
.rb-shots { display: flex; flex-wrap: wrap; gap: var(--u-s-2); }
|
|
1923
|
+
.rb-shot { display: flex; flex-direction: column; gap: 4px; }
|
|
1924
|
+
.rb-shot-box {
|
|
1925
|
+
position: relative;
|
|
1926
|
+
max-width: 260px;
|
|
1927
|
+
border: 1px solid var(--u-border);
|
|
1928
|
+
background: var(--u-bg-0);
|
|
1929
|
+
user-select: none;
|
|
1930
|
+
}
|
|
1931
|
+
.rb-shot-box img { display: block; max-width: 100%; height: auto; }
|
|
1932
|
+
.rb-shot-box.is-redacting { cursor: crosshair; }
|
|
1933
|
+
.rb-shot-box.is-redacting img { pointer-events: none; }
|
|
1934
|
+
.rb-redact-rect { position: absolute; background: #000; pointer-events: none; }
|
|
1935
|
+
.rb-shot-actions { display: flex; gap: var(--u-s-2); }
|
|
1936
|
+
.rb-link {
|
|
1937
|
+
background: transparent;
|
|
1938
|
+
border: none;
|
|
1939
|
+
padding: 0;
|
|
1940
|
+
color: var(--u-fg-1);
|
|
1941
|
+
text-decoration: underline;
|
|
1942
|
+
cursor: pointer;
|
|
1943
|
+
font-family: var(--u-font-mono);
|
|
1944
|
+
font-size: var(--type-xs);
|
|
1945
|
+
}
|
|
1946
|
+
.rb-link:hover, .rb-link.is-on { color: var(--u-fg-0); }
|
|
1947
|
+
.rb-actions { display: flex; justify-content: flex-end; gap: var(--u-s-2); margin-top: var(--u-s-2); }
|
|
1948
|
+
.rb-btn {
|
|
1949
|
+
background: var(--u-bg-2);
|
|
1950
|
+
border: 1px solid var(--u-fg-0);
|
|
1951
|
+
color: var(--u-fg-0);
|
|
1952
|
+
font-family: var(--u-font-mono);
|
|
1953
|
+
font-size: var(--type-xs);
|
|
1954
|
+
padding: var(--u-s-1) var(--u-s-3);
|
|
1955
|
+
cursor: pointer;
|
|
1956
|
+
}
|
|
1957
|
+
.rb-btn:hover { background: var(--u-bg-3); }
|
|
1958
|
+
.rb-btn:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
|
|
1959
|
+
.rb-btn[disabled] { opacity: 0.5; cursor: not-allowed; }
|
|
1960
|
+
.rb-primary { background: var(--u-fg-0); color: var(--u-bg-0); }
|
|
1961
|
+
.rb-primary:hover { background: var(--u-fg-1); }
|