SystemHealthPanel.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import type { ElementType } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { XCircle, AlertTriangle, CheckCircle2, ExternalLink, Wrench, ServerCog, Bug } from 'lucide-react';
  4. import type { LogFinding, LogFindingCategory, SystemHealthResult } from '../api/client';
  5. const WIKI_TROUBLESHOOTING = 'https://wiki.bambuddy.cool/reference/troubleshooting/';
  6. const CATEGORY_META: Record<LogFindingCategory, { icon: ElementType; badgeClass: string }> = {
  7. layer8: { icon: Wrench, badgeClass: 'bg-bambu-green/15 text-bambu-green border-bambu-green/30' },
  8. environment: { icon: ServerCog, badgeClass: 'bg-amber-100 dark:bg-amber-500/15 text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/30' },
  9. bug: { icon: Bug, badgeClass: 'bg-red-100 dark:bg-red-500/15 text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/30' },
  10. };
  11. /**
  12. * One detected log-health finding. Cause/fix/name text is rendered from i18n
  13. * keyed by signature_id; an unknown signature (frontend older than backend)
  14. * still renders gracefully via the defaultValue fallbacks.
  15. */
  16. function FindingCard({ finding }: { finding: LogFinding }) {
  17. const { t } = useTranslation();
  18. const id = finding.signature_id;
  19. const name = t(`systemHealth.signature.${id}.name`, { defaultValue: id });
  20. const cause = t(`systemHealth.signature.${id}.cause`, { defaultValue: '' });
  21. const fix = t(`systemHealth.signature.${id}.fix`, { defaultValue: '' });
  22. const meta = CATEGORY_META[finding.category] ?? CATEGORY_META.bug;
  23. const CategoryIcon = meta.icon;
  24. const SeverityIcon = finding.severity === 'error' ? XCircle : AlertTriangle;
  25. const severityColor = finding.severity === 'error' ? 'text-red-600 dark:text-red-400' : 'text-amber-600 dark:text-amber-400';
  26. return (
  27. <div className="bg-bambu-dark rounded-lg border border-bambu-dark-tertiary p-4 space-y-2">
  28. <div className="flex items-start gap-3">
  29. <SeverityIcon className={`w-5 h-5 flex-shrink-0 mt-0.5 ${severityColor}`} />
  30. <div className="flex-1 min-w-0">
  31. <div className="flex items-center gap-2 flex-wrap">
  32. <span className="text-sm font-medium text-white">{name}</span>
  33. <span
  34. className={`inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded-full border ${meta.badgeClass}`}
  35. >
  36. <CategoryIcon className="w-3 h-3" />
  37. {t(`systemHealth.category.${finding.category}`)}
  38. </span>
  39. </div>
  40. {cause && <p className="text-xs text-bambu-gray mt-1">{cause}</p>}
  41. </div>
  42. </div>
  43. {fix && (
  44. <div className="text-xs text-white/90 bg-bambu-dark-secondary rounded px-3 py-2">
  45. <span className="text-bambu-green font-medium">{t('systemHealth.fixLabel')}</span> {fix}
  46. </div>
  47. )}
  48. {finding.sample && (
  49. <div className="text-xs font-mono text-bambu-gray/70 bg-bambu-dark-secondary rounded px-3 py-2 break-all">
  50. {finding.sample}
  51. </div>
  52. )}
  53. <div className="flex items-center justify-between gap-2 flex-wrap">
  54. <span className="text-xs text-bambu-gray">
  55. {t('systemHealth.occurrences', { times: finding.count, lastSeen: finding.last_seen })}
  56. </span>
  57. <a
  58. href={`${WIKI_TROUBLESHOOTING}#${finding.wiki_anchor}`}
  59. target="_blank"
  60. rel="noopener noreferrer"
  61. className="inline-flex items-center gap-1 text-xs text-bambu-green hover:underline"
  62. >
  63. {t('systemHealth.learnMore')}
  64. <ExternalLink className="w-3 h-3" />
  65. </a>
  66. </div>
  67. </div>
  68. );
  69. }
  70. /**
  71. * Presentational panel for a log-health scan result. Shared by the System page
  72. * section and the bug-report bubble so both surfaces look identical.
  73. */
  74. export function SystemHealthPanel({ result }: { result: SystemHealthResult }) {
  75. const { t } = useTranslation();
  76. if (!result.log_available) {
  77. return (
  78. <div className="rounded-lg bg-amber-50 dark:bg-amber-500/10 border border-amber-300 dark:border-amber-500/30 px-4 py-3 text-sm text-amber-700 dark:text-amber-300">
  79. {t('systemHealth.logUnavailable')}
  80. </div>
  81. );
  82. }
  83. if (result.findings.length === 0) {
  84. return (
  85. <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">
  86. <CheckCircle2 className="w-5 h-5 flex-shrink-0" />
  87. <span>{t('systemHealth.clean', { times: result.scanned_entries })}</span>
  88. </div>
  89. );
  90. }
  91. return (
  92. <div className="space-y-3">
  93. {result.findings.map((finding) => (
  94. <FindingCard key={finding.signature_id} finding={finding} />
  95. ))}
  96. </div>
  97. );
  98. }