mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
feat(system): log-health scanner + Add/Edit-Printer setup pre-flight
Adds a passive log-health check that complements the active Connection Diagnostic. Scans Bambuddy's recent app log against a curated allowlist catalog of known failure signatures (rejected access code, FTPS :990 timeout, FTPS TLS failure, flapping MQTT, unreachable camera, SQLite "database is locked" contention), dedupes and classifies each finding as layer8/environment/bug, and deep-links to the troubleshooting wiki. Sample log lines are sanitized before they leave the process. Exposed via GET /system/health and surfaced on two surfaces sharing one SystemHealthPanel component: a System Health section on the System page, and inline in the bug reporter when the form opens. The Add-Printer and Edit-Printer dialogs gained a setup-time pre-flight: saving runs the connection diagnostic and, on a failed check, warns with a "save anyway" escape hatch instead of silently saving a printer that will immediately show offline. Log read/parse/sanitize primitives extracted from routes/support.py into a shared services/log_reader.py (behaviour-preserving); affected support tests repointed accordingly. Tests: test_log_health.py (11), test_system_api.py (2 new), SystemHealthPanel + BugReportBubble + AddPrinterPreflight + EditPrinterPreflight (8 frontend). All strings translated across the 9 locales. Backend ruff clean, full unit suite green, frontend build + eslint clean, i18n parity green.
This commit is contained in:
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api, bugReportApi, type PrinterDiagnosticResult } from '../api/client';
|
||||
import { DiagnosticChecklist } from './ConnectionDiagnostic';
|
||||
import { SystemHealthPanel } from './SystemHealthPanel';
|
||||
import { Collapsible } from './Collapsible';
|
||||
|
||||
type ViewState = 'form' | 'logging' | 'stopping' | 'submitting' | 'success' | 'error';
|
||||
@@ -85,6 +86,18 @@ export function BugReportBubble() {
|
||||
const diagnosticEntries = diagnosticScan.data ?? [];
|
||||
const diagnosticProblems = diagnosticEntries.filter((e) => e.result.overall === 'problems');
|
||||
|
||||
// Scan recent logs against the known-issue catalog. Like the diagnostic
|
||||
// above, this surfaces user-fixable ("layer 8") problems before a report is
|
||||
// filed. Only shown when something matched — a clean scan stays silent so
|
||||
// the form is uncluttered.
|
||||
const logHealthScan = useQuery({
|
||||
queryKey: ['bugReportLogHealth'],
|
||||
enabled: isOpen && viewState === 'form',
|
||||
staleTime: 30_000,
|
||||
queryFn: api.getSystemHealth,
|
||||
});
|
||||
const logFindings = logHealthScan.data?.findings ?? [];
|
||||
|
||||
// Elapsed timer for logging phase — auto-stop at 5 minutes
|
||||
useEffect(() => {
|
||||
if (viewState !== 'logging') return;
|
||||
@@ -299,6 +312,25 @@ export function BugReportBubble() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Log-health scan — known issues found in recent logs.
|
||||
Shown only when something matched. */}
|
||||
{!logHealthScan.isLoading && logFindings.length > 0 && logHealthScan.data && (
|
||||
<div className="rounded-lg bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-3 space-y-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<Stethoscope className="w-4 h-4 mt-0.5 flex-shrink-0 text-amber-600 dark:text-amber-400" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-amber-700 dark:text-amber-300">
|
||||
{t('bugReport.logHealthSummary')}
|
||||
</p>
|
||||
<p className="text-xs text-amber-800 dark:text-amber-200 mt-0.5">
|
||||
{t('bugReport.logHealthIntro')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<SystemHealthPanel result={logHealthScan.data} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import type { ElementType } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { XCircle, AlertTriangle, CheckCircle2, ExternalLink, Wrench, ServerCog, Bug } from 'lucide-react';
|
||||
import type { LogFinding, LogFindingCategory, SystemHealthResult } from '../api/client';
|
||||
|
||||
const WIKI_TROUBLESHOOTING = 'https://wiki.bambuddy.cool/reference/troubleshooting/';
|
||||
|
||||
const CATEGORY_META: Record<LogFindingCategory, { icon: ElementType; badgeClass: string }> = {
|
||||
layer8: { icon: Wrench, badgeClass: 'bg-bambu-green/15 text-bambu-green border-bambu-green/30' },
|
||||
environment: { icon: ServerCog, badgeClass: 'bg-amber-500/15 text-amber-300 border-amber-500/30' },
|
||||
bug: { icon: Bug, badgeClass: 'bg-red-500/15 text-red-300 border-red-500/30' },
|
||||
};
|
||||
|
||||
/**
|
||||
* One detected log-health finding. Cause/fix/name text is rendered from i18n
|
||||
* keyed by signature_id; an unknown signature (frontend older than backend)
|
||||
* still renders gracefully via the defaultValue fallbacks.
|
||||
*/
|
||||
function FindingCard({ finding }: { finding: LogFinding }) {
|
||||
const { t } = useTranslation();
|
||||
const id = finding.signature_id;
|
||||
const name = t(`systemHealth.signature.${id}.name`, { defaultValue: id });
|
||||
const cause = t(`systemHealth.signature.${id}.cause`, { defaultValue: '' });
|
||||
const fix = t(`systemHealth.signature.${id}.fix`, { defaultValue: '' });
|
||||
const meta = CATEGORY_META[finding.category] ?? CATEGORY_META.bug;
|
||||
const CategoryIcon = meta.icon;
|
||||
const SeverityIcon = finding.severity === 'error' ? XCircle : AlertTriangle;
|
||||
const severityColor = finding.severity === 'error' ? 'text-red-400' : 'text-amber-400';
|
||||
|
||||
return (
|
||||
<div className="bg-bambu-dark rounded-lg border border-bambu-dark-tertiary p-4 space-y-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<SeverityIcon className={`w-5 h-5 flex-shrink-0 mt-0.5 ${severityColor}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-medium text-white">{name}</span>
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full border ${meta.badgeClass}`}
|
||||
>
|
||||
<CategoryIcon className="w-3 h-3" />
|
||||
{t(`systemHealth.category.${finding.category}`)}
|
||||
</span>
|
||||
</div>
|
||||
{cause && <p className="text-xs text-bambu-gray mt-1">{cause}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{fix && (
|
||||
<div className="text-xs text-white/90 bg-bambu-dark-secondary rounded px-3 py-2">
|
||||
<span className="text-bambu-green font-medium">{t('systemHealth.fixLabel')}</span> {fix}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-xs font-mono text-bambu-gray/70 bg-bambu-dark-secondary rounded px-3 py-2 break-all">
|
||||
{finding.sample}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||
<span className="text-xs text-bambu-gray">
|
||||
{t('systemHealth.occurrences', { times: finding.count, lastSeen: finding.last_seen })}
|
||||
</span>
|
||||
<a
|
||||
href={`${WIKI_TROUBLESHOOTING}#${finding.wiki_anchor}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-1 text-xs text-bambu-green hover:underline"
|
||||
>
|
||||
{t('systemHealth.learnMore')}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Presentational panel for a log-health scan result. Shared by the System page
|
||||
* section and the bug-report bubble so both surfaces look identical.
|
||||
*/
|
||||
export function SystemHealthPanel({ result }: { result: SystemHealthResult }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!result.log_available) {
|
||||
return (
|
||||
<div className="rounded-lg bg-amber-500/10 border border-amber-500/30 px-4 py-3 text-sm text-amber-300">
|
||||
{t('systemHealth.logUnavailable')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (result.findings.length === 0) {
|
||||
return (
|
||||
<div className="rounded-lg bg-bambu-green/10 border border-bambu-green/30 px-4 py-3 text-sm text-bambu-green flex items-center gap-2">
|
||||
<CheckCircle2 className="w-5 h-5 flex-shrink-0" />
|
||||
<span>{t('systemHealth.clean', { times: result.scanned_entries })}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{result.findings.map((finding) => (
|
||||
<FindingCard key={finding.signature_id} finding={finding} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user