[Feature] Redesign bug report debug log capture flow

Replace fixed 30-second debug log collection with an interactive
  3-step flow: start logging, reproduce the issue, stop & submit.
  Users now control timing instead of racing a countdown.

  Backend: split _collect_debug_logs() into POST /start-logging and
  POST /stop-logging endpoints; add debug_logs field to submit request.
  Frontend: 3-step progress indicator with elapsed timer, pulsing
  active state, and 5-minute auto-stop. Updated all 7 locale files.
This commit is contained in:
maziggy
2026-03-20 14:18:24 +01:00
parent e82362ac65
commit dc4d77b93a
18 changed files with 800 additions and 550 deletions
+1
View File
@@ -34,6 +34,7 @@ All notable changes to Bambuddy will be documented in this file.
- **Spool Name Column & Filter in Filament Inventory** ([#740](https://github.com/maziggy/bambuddy/issues/740)) — Added a "Spool" column to the filament inventory table that displays the spool catalog entry name (e.g. "Bambu Lab AMS Tray", "Sunlu 1kg"). Enable it via the column visibility menu. Sortable and hidden by default. Also added a spool name filter dropdown next to the brand filter for quick filtering by spool type. Requested by @DMoenning.
### Changed
- **Redesigned Bug Report Debug Log Flow** — Replaced the fixed 30-second debug log collection with an interactive 3-step flow: start debug logging, reproduce the issue at your own pace, then stop & submit. An elapsed timer shows recording duration with auto-stop at 5 minutes. Users now have full control over when to capture logs instead of racing a countdown. The backend splits log collection into separate start/stop endpoints, and the frontend shows a step progress indicator with pulsing active state.
### Improved
- **Print Command Response Verification** ([#737](https://github.com/maziggy/bambuddy/issues/737)) — After sending a print command, BambuBuddy now monitors whether the printer's state changes within 15 seconds. If the printer silently ignores the command (observed on some P1S firmware versions where the MQTT command handler becomes unresponsive), a warning is logged for diagnostics. This aids debugging when users report prints not starting despite BambuBuddy showing success.
+1 -1
View File
@@ -198,7 +198,7 @@ Perfect for remote print farms, traveling makers, or accessing your home printer
- Debug logging toggle with live indicator
- Live application log viewer with filtering
- Support bundle generator with comprehensive diagnostics (privacy-filtered)
- **In-app bug reporting** — Submit bug reports directly from the UI with optional screenshot (upload, paste, or drag & drop), automatic diagnostic log collection (30s debug capture with printer push), and system info. Reports create GitHub issues via a secure relay. Privacy-first: all logs are sanitized and sensitive data (IPs, serials, credentials) is never included.
- **In-app bug reporting** — Submit bug reports directly from the UI with optional screenshot (upload, paste, or drag & drop), interactive debug log capture (start logging, reproduce at your own pace, stop & submit), and system info. Reports create GitHub issues via a secure relay. Privacy-first: all logs are sanitized and sensitive data (IPs, serials, credentials) is never included.
### 🔒 Optional Authentication
- Enable/disable authentication any time
+23 -18
View File
@@ -1,9 +1,8 @@
"""Bug report endpoint for submitting user bug reports to GitHub."""
import asyncio
import logging
from fastapi import APIRouter
from fastapi import APIRouter, Query
from pydantic import BaseModel
from backend.app.api.routes.support import (
@@ -20,14 +19,13 @@ from backend.app.services.printer_manager import printer_manager
router = APIRouter(prefix="/bug-report", tags=["bug-report"])
logger = logging.getLogger(__name__)
LOG_COLLECTION_SECONDS = 30
class BugReportRequest(BaseModel):
description: str
email: str | None = None
screenshot_base64: str | None = None
include_support_info: bool = True
debug_logs: str | None = None
class BugReportResponse(BaseModel):
@@ -37,40 +35,48 @@ class BugReportResponse(BaseModel):
issue_number: int | None = None
async def _collect_debug_logs() -> str:
"""Enable debug logging, push all printers, wait, then collect logs."""
# Check if debug was already enabled
class StartLoggingResponse(BaseModel):
started: bool
was_debug: bool
class StopLoggingResponse(BaseModel):
logs: str
@router.post("/start-logging", response_model=StartLoggingResponse)
async def start_logging():
"""Enable debug logging and push all printers for fresh data."""
async with async_session() as db:
was_debug, _ = await _get_debug_setting(db)
# Enable debug logging
if not was_debug:
async with async_session() as db:
await _set_debug_setting(db, True)
_apply_log_level(True)
logger.info("Bug report: temporarily enabled debug logging")
logger.info("Bug report: enabled debug logging")
# Send push_all to all connected printers
for printer_id in list(printer_manager._clients.keys()):
try:
printer_manager.request_status_update(printer_id)
except Exception:
logger.debug("Failed to push_all for printer %s", printer_id)
# Wait for logs to accumulate
await asyncio.sleep(LOG_COLLECTION_SECONDS)
return StartLoggingResponse(started=True, was_debug=was_debug)
# Collect logs
@router.post("/stop-logging", response_model=StopLoggingResponse)
async def stop_logging(was_debug: bool = Query(default=False)):
"""Collect logs and restore previous log level."""
logs = await _get_recent_sanitized_logs()
# Restore previous log level if it wasn't debug before
if not was_debug:
async with async_session() as db:
await _set_debug_setting(db, False)
_apply_log_level(False)
logger.info("Bug report: restored normal logging")
return logs
return StopLoggingResponse(logs=logs)
@router.post("/submit", response_model=BugReportResponse)
@@ -80,9 +86,8 @@ async def submit_bug_report(report: BugReportRequest):
if report.include_support_info:
try:
support_info = await _collect_support_info()
logs = await _collect_debug_logs()
if logs:
support_info["recent_logs"] = logs
if report.debug_logs:
support_info["recent_logs"] = report.debug_logs
except Exception:
logger.exception("Failed to collect support info for bug report")
+130 -26
View File
@@ -212,17 +212,16 @@ class TestBugReportService:
assert "GitHub API error" in result["message"]
class TestCollectDebugLogs:
"""Tests for _collect_debug_logs()."""
class TestStartLogging:
"""Tests for the start-logging endpoint handler."""
@pytest.mark.asyncio
@pytest.mark.unit
async def test_enables_debug_when_not_already_enabled(self):
"""Debug logging is enabled, then restored after collection."""
from backend.app.api.routes.bug_report import _collect_debug_logs
"""Debug logging is enabled and printers are pushed."""
from backend.app.api.routes.bug_report import start_logging
apply_calls = []
mock_db = AsyncMock()
with (
@@ -234,25 +233,26 @@ class TestCollectDebugLogs:
side_effect=lambda v: apply_calls.append(v),
),
patch("backend.app.api.routes.bug_report.printer_manager") as mock_pm,
patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value="DEBUG log line"),
patch("backend.app.api.routes.bug_report.asyncio.sleep", new_callable=AsyncMock),
patch("backend.app.api.routes.bug_report.LOG_COLLECTION_SECONDS", 0),
):
mock_pm._clients = {}
mock_pm._clients = {"printer1": MagicMock()}
mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
result = await _collect_debug_logs()
result = await start_logging()
assert result == "DEBUG log line"
assert apply_calls == [True, False] # enabled then restored
assert mock_set.call_count == 2
assert result.started is True
assert result.was_debug is False
assert apply_calls == [True]
mock_set.assert_called_once()
mock_pm.request_status_update.assert_called_once_with("printer1")
@pytest.mark.asyncio
@pytest.mark.unit
async def test_skips_enable_when_already_debug(self):
"""Debug logging not toggled when already enabled."""
from backend.app.api.routes.bug_report import _collect_debug_logs
mock_db = AsyncMock()
from backend.app.api.routes.bug_report import start_logging
with (
patch("backend.app.api.routes.bug_report.async_session") as mock_session,
@@ -260,18 +260,15 @@ class TestCollectDebugLogs:
patch("backend.app.api.routes.bug_report._set_debug_setting", new_callable=AsyncMock) as mock_set,
patch("backend.app.api.routes.bug_report._apply_log_level") as mock_apply,
patch("backend.app.api.routes.bug_report.printer_manager") as mock_pm,
patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value="logs"),
patch("backend.app.api.routes.bug_report.asyncio.sleep", new_callable=AsyncMock),
patch("backend.app.api.routes.bug_report.LOG_COLLECTION_SECONDS", 0),
):
mock_pm._clients = {}
mock_db = AsyncMock()
mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
result = await _collect_debug_logs()
result = await start_logging()
assert result == "logs"
assert result.started is True
assert result.was_debug is True
mock_apply.assert_not_called()
mock_set.assert_not_called()
@@ -279,7 +276,9 @@ class TestCollectDebugLogs:
@pytest.mark.unit
async def test_pushes_all_connected_printers(self):
"""Sends status update request to all connected printers."""
from backend.app.api.routes.bug_report import _collect_debug_logs
mock_db = AsyncMock()
from backend.app.api.routes.bug_report import start_logging
with (
patch("backend.app.api.routes.bug_report.async_session") as mock_session,
@@ -287,18 +286,123 @@ class TestCollectDebugLogs:
patch("backend.app.api.routes.bug_report._set_debug_setting", new_callable=AsyncMock),
patch("backend.app.api.routes.bug_report._apply_log_level"),
patch("backend.app.api.routes.bug_report.printer_manager") as mock_pm,
patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value=""),
patch("backend.app.api.routes.bug_report.asyncio.sleep", new_callable=AsyncMock),
patch("backend.app.api.routes.bug_report.LOG_COLLECTION_SECONDS", 0),
):
mock_pm._clients = {"printer1": MagicMock(), "printer2": MagicMock()}
mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
await start_logging()
assert mock_pm.request_status_update.call_count == 2
class TestStopLogging:
"""Tests for the stop-logging endpoint handler."""
@pytest.mark.asyncio
@pytest.mark.unit
async def test_collects_logs_and_restores_level(self):
"""Collects logs and restores log level when was_debug=False."""
from backend.app.api.routes.bug_report import stop_logging
apply_calls = []
mock_db = AsyncMock()
with (
patch("backend.app.api.routes.bug_report.async_session") as mock_session,
patch("backend.app.api.routes.bug_report._set_debug_setting", new_callable=AsyncMock) as mock_set,
patch(
"backend.app.api.routes.bug_report._apply_log_level",
side_effect=lambda v: apply_calls.append(v),
),
patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value="DEBUG log line"),
):
mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
result = await stop_logging(was_debug=False)
assert result.logs == "DEBUG log line"
assert apply_calls == [False]
mock_set.assert_called_once()
@pytest.mark.asyncio
@pytest.mark.unit
async def test_skips_restore_when_was_debug(self):
"""Does not restore log level when was_debug=True."""
from backend.app.api.routes.bug_report import stop_logging
with (
patch("backend.app.api.routes.bug_report.async_session") as mock_session,
patch("backend.app.api.routes.bug_report._set_debug_setting", new_callable=AsyncMock) as mock_set,
patch("backend.app.api.routes.bug_report._apply_log_level") as mock_apply,
patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", return_value="logs"),
):
mock_db = AsyncMock()
mock_session.return_value.__aenter__ = AsyncMock(return_value=mock_db)
mock_session.return_value.__aexit__ = AsyncMock(return_value=False)
await _collect_debug_logs()
result = await stop_logging(was_debug=True)
assert mock_pm.request_status_update.call_count == 2
assert result.logs == "logs"
mock_apply.assert_not_called()
mock_set.assert_not_called()
class TestSubmitBugReportRoute:
"""Tests for the submit_bug_report route handler."""
@pytest.mark.asyncio
@pytest.mark.unit
async def test_uses_provided_debug_logs(self):
"""When debug_logs is provided, it is used as recent_logs."""
from backend.app.api.routes.bug_report import BugReportRequest, submit_bug_report
report = BugReportRequest(
description="Test bug",
debug_logs="pre-collected debug logs",
)
with (
patch("backend.app.api.routes.bug_report._collect_support_info", return_value={"version": "1.0"}),
patch("backend.app.api.routes.bug_report.submit_report", new_callable=AsyncMock) as mock_submit,
):
mock_submit.return_value = {
"success": True,
"message": "Created",
"issue_url": "https://github.com/maziggy/bambuddy/issues/1",
"issue_number": 1,
}
result = await submit_bug_report(report)
assert result.success is True
call_kwargs = mock_submit.call_args[1]
assert call_kwargs["support_info"]["recent_logs"] == "pre-collected debug logs"
@pytest.mark.asyncio
@pytest.mark.unit
async def test_no_logs_when_debug_logs_not_provided(self):
"""When debug_logs is None, recent_logs is not added."""
from backend.app.api.routes.bug_report import BugReportRequest, submit_bug_report
report = BugReportRequest(description="Test bug")
with (
patch("backend.app.api.routes.bug_report._collect_support_info", return_value={"version": "1.0"}),
patch("backend.app.api.routes.bug_report.submit_report", new_callable=AsyncMock) as mock_submit,
):
mock_submit.return_value = {
"success": True,
"message": "Created",
"issue_url": None,
"issue_number": None,
}
await submit_bug_report(report)
call_kwargs = mock_submit.call_args[1]
assert "recent_logs" not in call_kwargs["support_info"]
class TestRateLimit:
+8 -5
View File
@@ -327,11 +327,14 @@ ${CHANGELOG_NOTES}
EOF
)
# Delete existing release so the new one gets today's date
# (gh release edit only updates title/notes, not the creation timestamp)
if gh release view "v${DAILY_TAG}" >/dev/null 2>&1; then
echo " Deleting old release v${DAILY_TAG} (will recreate with today's date)..."
gh release delete "v${DAILY_TAG}" --yes --cleanup-tag
# Delete ALL old daily releases — only the latest daily build should exist
echo " Cleaning up old daily releases..."
OLD_DAILY_RELEASES=$(gh release list --limit 100 --json tagName --jq '.[] | select(.tagName | test("-daily\\.")) | .tagName' 2>/dev/null || true)
if [ -n "$OLD_DAILY_RELEASES" ]; then
while IFS= read -r old_tag; do
echo " Deleting old daily release: ${old_tag}..."
gh release delete "$old_tag" --yes --cleanup-tag 2>/dev/null || true
done <<< "$OLD_DAILY_RELEASES"
fi
# Create/move tag to current HEAD and push
@@ -23,6 +23,17 @@ function getSubmitButton() {
);
}
function setupLoggingEndpoints() {
server.use(
http.post('*/bug-report/start-logging', () => {
return HttpResponse.json({ started: true, was_debug: false });
}),
http.post('*/bug-report/stop-logging', () => {
return HttpResponse.json({ logs: 'test debug logs' });
})
);
}
describe('BugReportBubble', () => {
it('renders the floating bug button', () => {
render(<BugReportBubble />);
@@ -79,16 +90,9 @@ describe('BugReportBubble', () => {
expect(getSubmitButton()).not.toBeDisabled();
});
it('shows collecting state with countdown after submit', async () => {
it('shows logging state with step indicators after start', async () => {
const user = userEvent.setup();
// Delay the API response so we can see collecting state
server.use(
http.post('*/bug-report/submit', async () => {
await new Promise((resolve) => setTimeout(resolve, 60000));
return HttpResponse.json({ success: true, message: 'ok', issue_url: null, issue_number: null });
})
);
setupLoggingEndpoints();
render(<BugReportBubble />);
await user.click(screen.getByRole('button'));
@@ -98,16 +102,23 @@ describe('BugReportBubble', () => {
const submitBtn = getSubmitButton();
if (submitBtn) await user.click(submitBtn);
// Should show collecting state
// Should show step indicators and elapsed timer
await waitFor(() => {
const collectingText = screen.queryByText(/collecting|Collecting|収集|Sammeln|Collecte|Raccolta|Coletando|收集/i);
expect(collectingText).toBeInTheDocument();
const reproduceText = screen.queryByText(/reproduce|Reproduce|reproduzieren|reproduire|riproduci|再現|reproduza|重现/i);
expect(reproduceText).toBeInTheDocument();
});
// Should show elapsed timer (00:00 format)
await waitFor(() => {
const timer = screen.queryByText(/00:0/);
expect(timer).toBeInTheDocument();
});
});
it('shows success state after successful submission', async () => {
const user = userEvent.setup();
setupLoggingEndpoints();
server.use(
http.post('*/bug-report/submit', () => {
return HttpResponse.json({
@@ -127,17 +138,29 @@ describe('BugReportBubble', () => {
const submitBtn = getSubmitButton();
if (submitBtn) await user.click(submitBtn);
// Wait for logging state, then click stop
await waitFor(() => {
expect(screen.queryByText(/reproduce|Reproduce|reproduzieren|reproduire|riproduci|再現|reproduza|重现/i)).toBeInTheDocument();
});
// Find and click the Stop & Submit button
const stopBtn = screen.getAllByRole('button').find(
(b) => b.className.includes('bg-red-500') && !b.className.includes('rounded-full')
);
if (stopBtn) await user.click(stopBtn);
await waitFor(
() => {
expect(screen.getByText(/#42/)).toBeInTheDocument();
},
{ timeout: 35000 }
{ timeout: 10000 }
);
});
it('shows error state after failed submission', async () => {
const user = userEvent.setup();
setupLoggingEndpoints();
server.use(
http.post('*/bug-report/submit', () => {
return HttpResponse.json({
@@ -157,11 +180,21 @@ describe('BugReportBubble', () => {
const submitBtn = getSubmitButton();
if (submitBtn) await user.click(submitBtn);
// Wait for logging state, then click stop
await waitFor(() => {
expect(screen.queryByText(/reproduce|Reproduce|reproduzieren|reproduire|riproduci|再現|reproduza|重现/i)).toBeInTheDocument();
});
const stopBtn = screen.getAllByRole('button').find(
(b) => b.className.includes('bg-red-500') && !b.className.includes('rounded-full')
);
if (stopBtn) await user.click(stopBtn);
await waitFor(
() => {
expect(screen.getByText(/Relay not available/)).toBeInTheDocument();
},
{ timeout: 35000 }
{ timeout: 10000 }
);
});
+9
View File
@@ -5043,6 +5043,7 @@ export interface BugReportRequest {
email?: string;
screenshot_base64?: string;
include_support_info?: boolean;
debug_logs?: string;
}
export interface BugReportResponse {
@@ -5058,4 +5059,12 @@ export const bugReportApi = {
method: 'POST',
body: JSON.stringify(data),
}),
startLogging: () =>
request<{ started: boolean; was_debug: boolean }>('/bug-report/start-logging', {
method: 'POST',
}),
stopLogging: (wasDebug: boolean) =>
request<{ logs: string }>(`/bug-report/stop-logging?was_debug=${wasDebug}`, {
method: 'POST',
}),
};
+95 -28
View File
@@ -1,14 +1,13 @@
import { useState, useRef, useCallback, useEffect } from 'react';
import { Bug, X, Loader2, CheckCircle, AlertCircle, Trash2, Upload } from 'lucide-react';
import { Bug, X, Loader2, CheckCircle, AlertCircle, Trash2, Upload, Circle, CheckCircle2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { bugReportApi } from '../api/client';
type ViewState = 'form' | 'collecting' | 'submitting' | 'success' | 'error';
const LOG_COLLECTION_SECONDS = 30;
type ViewState = 'form' | 'logging' | 'stopping' | 'submitting' | 'success' | 'error';
const MAX_DIMENSION = 1920;
const JPEG_QUALITY = 0.7;
const MAX_LOG_SECONDS = 300; // 5 minutes
function compressImage(file: File): Promise<string> {
return new Promise((resolve, reject) => {
@@ -34,6 +33,12 @@ function compressImage(file: File): Promise<string> {
});
}
function formatElapsed(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
}
export function BugReportBubble() {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
@@ -45,20 +50,22 @@ export function BugReportBubble() {
const [issueUrl, setIssueUrl] = useState<string | null>(null);
const [issueNumber, setIssueNumber] = useState<number | null>(null);
const [errorMessage, setErrorMessage] = useState('');
const [countdown, setCountdown] = useState(0);
const [elapsedSeconds, setElapsedSeconds] = useState(0);
const [wasDebug, setWasDebug] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleStopLoggingRef = useRef<() => void>(() => {});
// Countdown timer for log collection phase
// Elapsed timer for logging phase — auto-stop at 5 minutes
useEffect(() => {
if (viewState !== 'collecting') return;
if (countdown <= 0) {
setViewState('submitting');
if (viewState !== 'logging') return;
if (elapsedSeconds >= MAX_LOG_SECONDS) {
handleStopLoggingRef.current();
return;
}
const timer = setTimeout(() => setCountdown((c) => c - 1), 1000);
const timer = setTimeout(() => setElapsedSeconds((s) => s + 1), 1000);
return () => clearTimeout(timer);
}, [viewState, countdown]);
}, [viewState, elapsedSeconds]);
const handleOpen = () => {
setIsOpen(true);
@@ -69,6 +76,8 @@ export function BugReportBubble() {
setIssueUrl(null);
setIssueNumber(null);
setErrorMessage('');
setElapsedSeconds(0);
setWasDebug(false);
};
const handleClose = () => {
@@ -114,16 +123,40 @@ export function BugReportBubble() {
if (file) handleFile(file);
}, [handleFile]);
const handleSubmit = async () => {
const handleStartLogging = async () => {
if (!description.trim()) return;
setCountdown(LOG_COLLECTION_SECONDS);
setViewState('collecting');
try {
const result = await bugReportApi.startLogging();
setWasDebug(result.was_debug);
setElapsedSeconds(0);
setViewState('logging');
} catch (err) {
setErrorMessage(err instanceof Error ? err.message : t('bugReport.unexpectedError'));
setViewState('error');
}
};
const handleStopLogging = async () => {
setViewState('stopping');
try {
const stopResult = await bugReportApi.stopLogging(wasDebug);
await handleSubmitReport(stopResult.logs);
} catch (err) {
setErrorMessage(err instanceof Error ? err.message : t('bugReport.unexpectedError'));
setViewState('error');
}
};
handleStopLoggingRef.current = handleStopLogging;
const handleSubmitReport = async (debugLogs: string) => {
setViewState('submitting');
try {
const result = await bugReportApi.submit({
description: description.trim(),
email: email.trim() || undefined,
screenshot_base64: screenshot || undefined,
include_support_info: true,
debug_logs: debugLogs || undefined,
});
if (result.success) {
setIssueUrl(result.issue_url || null);
@@ -281,30 +314,64 @@ export function BugReportBubble() {
{t('common.cancel')}
</button>
<button
onClick={handleSubmit}
onClick={handleStartLogging}
disabled={!description.trim()}
className="px-4 py-2 text-sm font-medium text-white bg-red-500 hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-colors"
>
{t('bugReport.submit')}
{t('bugReport.startLogging')}
</button>
</div>
</>
)}
{(viewState === 'collecting' || viewState === 'submitting') && (
{viewState === 'logging' && (
<div className="py-6 space-y-6">
{/* 3-step progress indicator */}
<div className="space-y-3 px-2">
{/* Step 1: Completed */}
<div className="flex items-center gap-3">
<CheckCircle2 className="w-5 h-5 text-green-500 flex-shrink-0" />
<span className="text-sm text-green-700 dark:text-green-400">{t('bugReport.stepEnableLogging')}</span>
</div>
{/* Step 2: Active */}
<div className="flex items-center gap-3">
<span className="relative flex h-5 w-5 flex-shrink-0 items-center justify-center">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-blue-500"></span>
</span>
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">{t('bugReport.stepReproduce')}</span>
</div>
{/* Step 3: Upcoming */}
<div className="flex items-center gap-3">
<Circle className="w-5 h-5 text-gray-300 dark:text-gray-600 flex-shrink-0" />
<span className="text-sm text-gray-400 dark:text-gray-500">{t('bugReport.stepStopLogging')}</span>
</div>
</div>
{/* Elapsed timer */}
<div className="text-center">
<p className="text-3xl font-mono text-blue-500">{formatElapsed(elapsedSeconds)}</p>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{t('bugReport.maxDuration', { minutes: 5 })}</p>
</div>
{/* Stop & Submit button */}
<div className="flex justify-center">
<button
onClick={handleStopLogging}
className="px-6 py-2.5 text-sm font-medium text-white bg-red-500 hover:bg-red-600 rounded-lg transition-colors"
>
{t('bugReport.stopAndSubmit')}
</button>
</div>
</div>
)}
{(viewState === 'stopping' || viewState === 'submitting') && (
<div className="flex flex-col items-center justify-center py-8 gap-3">
<Loader2 className="w-8 h-8 animate-spin text-blue-500" />
{viewState === 'collecting' ? (
<>
<p className="text-sm font-medium text-gray-900 dark:text-white">{t('bugReport.collectingLogs')}</p>
<p className="text-xs text-gray-500 dark:text-gray-400">{t('bugReport.collectingLogsHint')}</p>
{countdown > 0 && (
<p className="text-lg font-mono text-blue-500">{t('bugReport.countdownSeconds', { seconds: countdown })}</p>
)}
</>
) : (
<p className="text-sm text-gray-600 dark:text-gray-400">{t('bugReport.submitting')}</p>
)}
<p className="text-sm text-gray-600 dark:text-gray-400">
{viewState === 'stopping' ? t('bugReport.stoppingLogs') : t('bugReport.submitting')}
</p>
</div>
)}
+7 -3
View File
@@ -4525,8 +4525,13 @@ export default {
dataNeverIncluded: 'Nie enthalten:',
dataNeverIncludedList: 'Druckernamen, Seriennummern, Zugangscodes, Passwörter, IP-Adressen, E-Mail-Adressen, API-Schlüssel, Tokens, Webhook-URLs, Hostnamen oder Benutzernamen.',
submit: 'Absenden',
collectingLogs: 'Diagnoseprotokolle werden gesammelt...',
collectingLogsHint: 'Debug-Protokollierung aktiviert, Drucker werden nach aktuellen Daten abgefragt.',
startLogging: 'Debug-Protokollierung starten',
stepEnableLogging: 'Debug-Protokollierung aktiviert',
stepReproduce: 'Problem jetzt reproduzieren',
stepStopLogging: 'Stoppen & Bericht senden',
stopAndSubmit: 'Stoppen & Senden',
maxDuration: 'Stoppt automatisch nach {{minutes}} Min.',
stoppingLogs: 'Protokolle sammeln & senden...',
submitting: 'Fehlerbericht wird gesendet...',
submitSuccess: 'Fehlerbericht erfolgreich gesendet!',
submitFailed: 'Fehlerbericht konnte nicht gesendet werden',
@@ -4534,6 +4539,5 @@ export default {
submitted: 'Ihr Fehlerbericht wurde eingereicht.',
viewIssue: 'Issue ansehen',
unexpectedError: 'Ein unerwarteter Fehler ist aufgetreten',
countdownSeconds: '{{seconds}}s',
},
};
+7 -3
View File
@@ -4530,8 +4530,13 @@ export default {
dataNeverIncluded: 'Never included:',
dataNeverIncludedList: 'Printer names, serial numbers, access codes, passwords, IP addresses, email addresses, API keys, tokens, webhook URLs, hostnames, or usernames.',
submit: 'Submit',
collectingLogs: 'Collecting diagnostic logs...',
collectingLogsHint: 'Debug logging enabled, querying printers for fresh data.',
startLogging: 'Start Debug Logging',
stepEnableLogging: 'Debug logging enabled',
stepReproduce: 'Reproduce the issue now',
stepStopLogging: 'Stop & submit report',
stopAndSubmit: 'Stop & Submit',
maxDuration: 'Auto-stops after {{minutes}} min',
stoppingLogs: 'Collecting logs & submitting...',
submitting: 'Submitting bug report...',
submitSuccess: 'Bug report submitted successfully!',
submitFailed: 'Failed to submit bug report',
@@ -4539,6 +4544,5 @@ export default {
submitted: 'Your bug report has been submitted.',
viewIssue: 'View Issue',
unexpectedError: 'An unexpected error occurred',
countdownSeconds: '{{seconds}}s',
},
};
+7 -3
View File
@@ -4517,8 +4517,13 @@ export default {
dataNeverIncluded: 'Jamais inclus :',
dataNeverIncludedList: 'Noms d\'imprimantes, numéros de série, codes d\'accès, mots de passe, adresses IP, adresses e-mail, clés API, tokens, URLs de webhook, noms d\'hôtes ou noms d\'utilisateurs.',
submit: 'Envoyer',
collectingLogs: 'Collecte des journaux de diagnostic...',
collectingLogsHint: 'Journalisation de débogage activée, interrogation des imprimantes pour des données fraîches.',
startLogging: 'Lancer la journalisation',
stepEnableLogging: 'Journalisation activée',
stepReproduce: 'Reproduisez le problème',
stepStopLogging: 'Arrêter & envoyer',
stopAndSubmit: 'Arrêter & Envoyer',
maxDuration: 'Arrêt auto après {{minutes}} min',
stoppingLogs: 'Collecte des journaux & envoi...',
submitting: 'Envoi du rapport de bug...',
submitSuccess: 'Rapport de bug envoyé avec succès !',
submitFailed: 'Échec de l\'envoi du rapport de bug',
@@ -4526,6 +4531,5 @@ export default {
submitted: 'Votre rapport de bug a été soumis.',
viewIssue: 'Voir l\'issue',
unexpectedError: 'Une erreur inattendue est survenue',
countdownSeconds: '{{seconds}}s',
},
};
+7 -3
View File
@@ -4516,8 +4516,13 @@ export default {
dataNeverIncluded: 'Mai inclusi:',
dataNeverIncludedList: 'Nomi stampanti, numeri di serie, codici di accesso, password, indirizzi IP, indirizzi email, chiavi API, token, URL webhook, nomi host o nomi utente.',
submit: 'Invia',
collectingLogs: 'Raccolta dei log diagnostici...',
collectingLogsHint: 'Registrazione debug attivata, interrogazione delle stampanti per dati aggiornati.',
startLogging: 'Avvia registrazione debug',
stepEnableLogging: 'Registrazione debug attivata',
stepReproduce: 'Riproduci il problema ora',
stepStopLogging: 'Ferma & invia rapporto',
stopAndSubmit: 'Ferma & Invia',
maxDuration: 'Arresto automatico dopo {{minutes}} min',
stoppingLogs: 'Raccolta log & invio...',
submitting: 'Invio segnalazione bug...',
submitSuccess: 'Segnalazione bug inviata con successo!',
submitFailed: 'Impossibile inviare la segnalazione bug',
@@ -4525,6 +4530,5 @@ export default {
submitted: 'La tua segnalazione bug è stata inviata.',
viewIssue: 'Vedi issue',
unexpectedError: 'Si è verificato un errore imprevisto',
countdownSeconds: '{{seconds}}s',
},
};
+7 -3
View File
@@ -4529,8 +4529,13 @@ export default {
dataNeverIncluded: '含まれないもの:',
dataNeverIncludedList: 'プリンター名、シリアル番号、アクセスコード、パスワード、IPアドレス、メールアドレス、APIキー、トークン、Webhook URL、ホスト名、ユーザー名。',
submit: '送信',
collectingLogs: '診断ログを収集中...',
collectingLogsHint: 'デバッグログを有効化し、プリンターから最新データを取得しています。',
startLogging: 'デバッグログ開始',
stepEnableLogging: 'デバッグログ有効',
stepReproduce: '問題を再現してください',
stepStopLogging: '停止してレポート送信',
stopAndSubmit: '停止して送信',
maxDuration: '{{minutes}}分後に自動停止',
stoppingLogs: 'ログ収集・送信中...',
submitting: 'バグレポートを送信中...',
submitSuccess: 'バグレポートが正常に送信されました!',
submitFailed: 'バグレポートの送信に失敗しました',
@@ -4538,6 +4543,5 @@ export default {
submitted: 'バグレポートが送信されました。',
viewIssue: 'Issueを表示',
unexpectedError: '予期しないエラーが発生しました',
countdownSeconds: '{{seconds}}秒',
},
};
+7 -3
View File
@@ -4516,8 +4516,13 @@ export default {
dataNeverIncluded: 'Nunca incluídos:',
dataNeverIncludedList: 'Nomes de impressoras, números de série, códigos de acesso, senhas, endereços IP, endereços de email, chaves de API, tokens, URLs de webhook, nomes de host ou nomes de usuário.',
submit: 'Enviar',
collectingLogs: 'Coletando logs de diagnóstico...',
collectingLogsHint: 'Log de depuração ativado, consultando impressoras para dados atualizados.',
startLogging: 'Iniciar log de depuração',
stepEnableLogging: 'Log de depuração ativado',
stepReproduce: 'Reproduza o problema agora',
stepStopLogging: 'Parar & enviar relatório',
stopAndSubmit: 'Parar & Enviar',
maxDuration: 'Para automaticamente após {{minutes}} min',
stoppingLogs: 'Coletando logs & enviando...',
submitting: 'Enviando relatório de bug...',
submitSuccess: 'Relatório de bug enviado com sucesso!',
submitFailed: 'Falha ao enviar relatório de bug',
@@ -4525,6 +4530,5 @@ export default {
submitted: 'Seu relatório de bug foi enviado.',
viewIssue: 'Ver issue',
unexpectedError: 'Ocorreu um erro inesperado',
countdownSeconds: '{{seconds}}s',
},
};
+7 -3
View File
@@ -4515,8 +4515,13 @@ export default {
dataNeverIncluded: '绝不包含:',
dataNeverIncludedList: '打印机名称、序列号、访问代码、密码、IP地址、邮箱地址、API密钥、令牌、Webhook URL、主机名或用户名。',
submit: '提交',
collectingLogs: '正在收集诊断日志...',
collectingLogsHint: '已启用调试日志,正在查询打印机获取最新数据。',
startLogging: '开始调试日志',
stepEnableLogging: '调试日志已启用',
stepReproduce: '请现在重现问题',
stepStopLogging: '停止并提交报告',
stopAndSubmit: '停止并提交',
maxDuration: '{{minutes}}分钟后自动停止',
stoppingLogs: '正在收集日志并提交...',
submitting: '正在提交错误报告...',
submitSuccess: '错误报告提交成功!',
submitFailed: '提交错误报告失败',
@@ -4524,6 +4529,5 @@ export default {
submitted: '您的错误报告已提交。',
viewIssue: '查看Issue',
unexpectedError: '发生了意外错误',
countdownSeconds: '{{seconds}}秒',
},
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,8 +23,8 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-Ce8mhR3n.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-hPK_4Ftq.css">
<script type="module" crossorigin src="/assets/index-Dwo_5Mjd.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DI9VPacx.css">
</head>
<body>
<div id="root"></div>