1use serde_json::json;
2use std::fmt::Write as _;
3use std::path::{Path, PathBuf};
4
5const REPORT_TOPICS: &[(&str, &str)] = &[
6 ("health_report", "System Health"),
7 ("hardware", "Hardware"),
8 ("storage", "Storage"),
9 ("network", "Network"),
10 ("security", "Security"),
11 ("toolchains", "Developer Toolchains"),
12];
13
14pub fn report_topics() -> &'static [(&'static str, &'static str)] {
15 REPORT_TOPICS
16}
17
18const TRIAGE_TOPICS: &[(&str, &str)] = &[
20 ("health_report", "System Health"),
21 ("security", "Security Posture"),
22 ("connectivity", "Connectivity"),
23 ("identity_auth", "Identity & Auth (M365/AAD)"),
24 ("updates", "Windows Updates"),
25];
26
27pub fn triage_topics_for_preset(preset: &str) -> &'static [(&'static str, &'static str)] {
28 match preset {
29 "network" => &[
30 ("connectivity", "Connectivity"),
31 ("wifi", "Wi-Fi"),
32 ("latency", "Latency"),
33 ("dns_servers", "DNS Servers"),
34 ("vpn", "VPN"),
35 ("proxy", "Proxy"),
36 ("connections", "Active Connections"),
37 ],
38 "security" => &[
39 ("security", "Security Posture"),
40 ("bitlocker", "BitLocker"),
41 ("tpm", "TPM / Secure Boot"),
42 ("local_security_policy", "Local Security Policy"),
43 ("shares", "SMB Shares"),
44 ("print_spooler", "Print Spooler"),
45 ],
46 "performance" => &[
47 ("resource_load", "Resource Load"),
48 ("thermal", "Thermal"),
49 ("cpu_power", "CPU Power"),
50 ("processes", "Top Processes"),
51 ("pagefile", "Page File"),
52 ("startup_items", "Startup Items"),
53 ],
54 "storage" => &[
55 ("storage", "Storage"),
56 ("disk_health", "Disk Health"),
57 ("shadow_copies", "Shadow Copies"),
58 ("storage_spaces", "Storage Spaces"),
59 ("bitlocker", "BitLocker"),
60 ],
61 "apps" => &[
62 ("browser_health", "Browser Health"),
63 ("outlook", "Outlook"),
64 ("teams", "Teams"),
65 ("installer_health", "Installer Health"),
66 ("onedrive", "OneDrive"),
67 ],
68 _ => TRIAGE_TOPICS,
69 }
70}
71
72fn triage_preset_title(preset: &str) -> &'static str {
73 match preset {
74 "network" => "Hematite Network Triage Report",
75 "security" => "Hematite Security Triage Report",
76 "performance" => "Hematite Performance Triage Report",
77 "storage" => "Hematite Storage Triage Report",
78 "apps" => "Hematite App Health Triage Report",
79 _ => "Hematite IT Triage Report",
80 }
81}
82
83fn topics_for_issue(issue: &str) -> Vec<(&'static str, &'static str)> {
85 let lower = issue.to_ascii_lowercase();
86 let mut seen = std::collections::HashSet::new();
87 let mut topics: Vec<(&'static str, &'static str)> = Vec::new();
88
89 macro_rules! add_if {
90 ($keywords:expr, $pairs:expr) => {
91 if $keywords.iter().any(|k: &&str| lower.contains(k)) {
92 for &pair in $pairs {
93 if seen.insert(pair.0) {
94 topics.push(pair);
95 }
96 }
97 }
98 };
99 }
100
101 add_if!(
102 &[
103 "slow",
104 "lag",
105 "freeze",
106 "hang",
107 "sluggish",
108 "unresponsive",
109 "performance",
110 "high cpu",
111 "high ram",
112 "high memory",
113 "locking up"
114 ],
115 &[
116 ("resource_load", "Resource Load"),
117 ("thermal", "Thermal"),
118 ("cpu_power", "CPU Power"),
119 ("pagefile", "Page File"),
120 ("storage", "Storage"),
121 ("startup_items", "Startup Items")
122 ]
123 );
124 add_if!(
125 &[
126 "internet",
127 "network",
128 "wifi",
129 "wi-fi",
130 "wireless",
131 "offline",
132 "no web",
133 "can't browse",
134 "ping fails",
135 "no connection",
136 "can't connect"
137 ],
138 &[
139 ("connectivity", "Connectivity"),
140 ("wifi", "Wi-Fi"),
141 ("latency", "Latency"),
142 ("dns_servers", "DNS Servers")
143 ]
144 );
145 add_if!(
146 &["dns ", "dns:", "name resolution", "can't resolve"],
147 &[
148 ("dns_servers", "DNS Servers"),
149 ("connectivity", "Connectivity")
150 ]
151 );
152 add_if!(
153 &[
154 "vpn ",
155 "vpn:",
156 "tunnel",
157 "remote access",
158 "wireguard",
159 "anyconnect",
160 "globalprotect",
161 "pulse secure",
162 "split tunnel"
163 ],
164 &[
165 ("vpn", "VPN"),
166 ("connectivity", "Connectivity"),
167 ("proxy", "Proxy")
168 ]
169 );
170 add_if!(
171 &[
172 "disk full",
173 "out of space",
174 "low disk",
175 "disk space",
176 "drive full",
177 "drive almost full",
178 "c drive",
179 "storage full",
180 "no space",
181 "disk filling"
182 ],
183 &[
184 ("storage", "Storage"),
185 ("disk_health", "Disk Health"),
186 ("shadow_copies", "Shadow Copies")
187 ]
188 );
189 add_if!(
190 &[
191 "disk fail",
192 "drive fail",
193 "smart error",
194 "disk error",
195 "bad sector",
196 "drive health",
197 "disk health",
198 "check disk"
199 ],
200 &[("disk_health", "Disk Health"), ("storage", "Storage")]
201 );
202 add_if!(
203 &[
204 "slow boot",
205 "boot slow",
206 "slow startup",
207 "startup slow",
208 "takes forever to boot",
209 "not booting",
210 "won't boot",
211 "wont boot",
212 "boot fail",
213 "boot loop",
214 "stuck on spinning",
215 "spinning wheel",
216 "loading wheel",
217 "infinite loading"
218 ],
219 &[
220 ("startup_items", "Startup Items"),
221 ("services", "Services"),
222 ("disk_health", "Disk Health")
223 ]
224 );
225 add_if!(
226 &[
227 "crash",
228 "bsod",
229 "blue screen",
230 "unexpected restart",
231 "unexpected shutdown",
232 "kernel panic",
233 "stop error",
234 "restart loop",
235 "reboot loop",
236 "boot loop",
237 "stuck on restart",
238 "stuck restarting",
239 "endless reboot"
240 ],
241 &[
242 ("recent_crashes", "Crash History"),
243 ("log_check", "Event Log"),
244 ("thermal", "Thermal"),
245 ("disk_health", "Disk Health")
246 ]
247 );
248 add_if!(
249 &[
250 "app crash",
251 "application crash",
252 "program crash",
253 "program not opening",
254 "app not starting",
255 "not responding",
256 "application error"
257 ],
258 &[
259 ("app_crashes", "Application Crashes"),
260 ("log_check", "Event Log")
261 ]
262 );
263 add_if!(
264 &[
265 "update",
266 "windows update",
267 "patch",
268 "stuck on update",
269 "update fail",
270 "wuauserv"
271 ],
272 &[
273 ("updates", "Windows Updates"),
274 ("pending_reboot", "Pending Reboot"),
275 ("services", "Services")
276 ]
277 );
278 add_if!(
279 &[
280 "virus",
281 "malware",
282 "hacked",
283 "suspicious",
284 "threat",
285 "infected",
286 "ransomware",
287 "defender blocked",
288 "windows defender",
289 "defender flagged",
290 "defender quarantine"
291 ],
292 &[
293 ("security", "Security Posture"),
294 ("defender_quarantine", "Defender Quarantine"),
295 ("log_check", "Event Log")
296 ]
297 );
298 add_if!(
299 &[
300 "firewall",
301 "blocked port",
302 "blocked connection",
303 "port block"
304 ],
305 &[
306 ("security", "Security Posture"),
307 ("firewall_rules", "Firewall Rules")
308 ]
309 );
310 add_if!(
311 &[
312 "printer",
313 "printing",
314 "print queue",
315 "can't print",
316 "print fail"
317 ],
318 &[
319 ("printers", "Printers"),
320 ("print_spooler", "Print Spooler"),
321 ("drivers", "Drivers")
322 ]
323 );
324 add_if!(
325 &[
326 "sound",
327 "audio",
328 "speaker",
329 "no sound",
330 "headset",
331 "microphone",
332 "mic ",
333 "mic not",
334 "mic stopped",
335 "crackling",
336 "audio fail"
337 ],
338 &[("audio", "Audio"), ("drivers", "Drivers")]
339 );
340 add_if!(
341 &[
342 "bluetooth",
343 "headphones",
344 "airpods",
345 "wireless headset",
346 "bt "
347 ],
348 &[
349 ("bluetooth", "Bluetooth"),
350 ("audio", "Audio"),
351 ("device_health", "Device Health")
352 ]
353 );
354 add_if!(
355 &[
356 "camera",
357 "webcam",
358 "video call",
359 "camera not working",
360 "can't see camera"
361 ],
362 &[("camera", "Camera")]
363 );
364 add_if!(
365 &["teams", "microsoft teams"],
366 &[
367 ("teams", "Teams"),
368 ("identity_auth", "Identity & Auth"),
369 ("browser_health", "Browser Health"),
370 ("connectivity", "Connectivity")
371 ]
372 );
373 add_if!(
374 &["outlook", "email not working", "mail not", "calendar not"],
375 &[
376 ("outlook", "Outlook"),
377 ("identity_auth", "Identity & Auth"),
378 ("connectivity", "Connectivity")
379 ]
380 );
381 add_if!(
382 &[
383 "browser",
384 "chrome",
385 "edge ",
386 "firefox",
387 "slow browser",
388 "browser crash",
389 "browser not"
390 ],
391 &[("browser_health", "Browser Health")]
392 );
393 add_if!(
394 &[
395 "sign in",
396 "can't log in",
397 "login fail",
398 "password",
399 "pin not working",
400 "pin broken",
401 "pin stopped",
402 "hello pin",
403 "windows hello",
404 "fingerprint",
405 "hello not",
406 "locked out",
407 "authentication fail"
408 ],
409 &[
410 ("sign_in", "Sign-In / Windows Hello"),
411 ("identity_auth", "Identity & Auth"),
412 ("credentials", "Credentials")
413 ]
414 );
415 add_if!(
416 &[
417 "rdp",
418 "remote desktop",
419 "can't connect remotely",
420 "remote desktop not"
421 ],
422 &[
423 ("rdp", "Remote Desktop"),
424 ("connectivity", "Connectivity"),
425 ("firewall_rules", "Firewall Rules")
426 ]
427 );
428 add_if!(
429 &[
430 "device not recognized",
431 "driver not",
432 "usb not working",
433 "device problem",
434 "yellow bang",
435 "hardware not"
436 ],
437 &[
438 ("device_health", "Device Health"),
439 ("drivers", "Drivers"),
440 ("peripherals", "Peripherals")
441 ]
442 );
443 add_if!(
444 &[
445 "time wrong",
446 "clock wrong",
447 "wrong time",
448 "time sync",
449 "time off",
450 "time zone",
451 "timezone",
452 "wrong timezone",
453 "ntp ",
454 "ntp:",
455 "w32tm",
456 "w32time",
457 "windows time service",
458 "time service stopped"
459 ],
460 &[("ntp", "NTP / Time Sync")]
461 );
462 add_if!(
463 &[
464 "onedrive",
465 "one drive",
466 "file sync",
467 "not syncing",
468 "onedrive sync"
469 ],
470 &[("onedrive", "OneDrive")]
471 );
472 add_if!(
473 &["wmi error", "powershell wmi", "get-wmiobject fail"],
474 &[("wmi_health", "WMI Health")]
475 );
476 add_if!(
477 &[
478 "monitor",
479 "display",
480 "screen resolution",
481 "screen brightness",
482 "brightness not working",
483 "brightness stuck",
484 "second monitor",
485 "wrong resolution",
486 "display settings",
487 "refresh rate",
488 "scaling"
489 ],
490 &[("display_config", "Display Config")]
491 );
492 add_if!(
493 &[
494 "keyboard not",
495 "keyboard stop",
496 "keyboard broke",
497 "mouse not",
498 "mouse stop",
499 "mouse broke",
500 "touchpad",
501 "trackpad",
502 "peripheral not",
503 "numpad",
504 "special keys",
505 "fn key",
506 "function key",
507 "key not responding",
508 "keys not working"
509 ],
510 &[
511 ("peripherals", "Peripherals"),
512 ("device_health", "Device Health")
513 ]
514 );
515 add_if!(
516 &[
517 "hibernate",
518 "won't hibernate",
519 "wont hibernate",
520 "sleep issue",
521 "won't sleep",
522 "wont sleep",
523 "wont go to sleep",
524 "won't go to sleep",
525 "won't wake",
526 "wont wake",
527 "stuck after sleep",
528 "won't wake up",
529 "sleep mode"
530 ],
531 &[
532 ("pending_reboot", "Pending Reboot"),
533 ("services", "Services"),
534 ("thermal", "Thermal")
535 ]
536 );
537 add_if!(
538 &[
539 "microsoft store",
540 "store app",
541 "windows store",
542 "uwp",
543 "app won't install",
544 "installer stuck",
545 "install stuck",
546 "setup stuck",
547 "store not working",
548 "winget"
549 ],
550 &[("installer_health", "Installer Health")]
551 );
552 add_if!(
553 &[
554 "no sound",
555 "audio not",
556 "sound not",
557 "speaker not",
558 "microphone not",
559 "mic not",
560 "audio stopped",
561 "crackling",
562 "no audio"
563 ],
564 &[("audio", "Audio")]
565 );
566 add_if!(
567 &[
568 "bluetooth not",
569 "bluetooth won't",
570 "headset won't connect",
571 "headphones won't",
572 "can't pair",
573 "won't pair",
574 "bluetooth disconnect",
575 "bluetooth keep"
576 ],
577 &[
578 ("bluetooth", "Bluetooth"),
579 ("device_health", "Device Health")
580 ]
581 );
582 add_if!(
583 &[
584 "outlook not",
585 "outlook won't",
586 "outlook crash",
587 "outlook slow",
588 "email not",
589 "email won't",
590 "email crash",
591 "calendar not",
592 "pst",
593 "ost file"
594 ],
595 &[("outlook", "Outlook")]
596 );
597 add_if!(
598 &[
599 "teams not",
600 "teams won't",
601 "teams crash",
602 "teams slow",
603 "teams black screen",
604 "teams audio",
605 "teams video",
606 "microsoft teams"
607 ],
608 &[("teams", "Teams")]
609 );
610 add_if!(
611 &[
612 "chrome slow",
613 "chrome crash",
614 "edge slow",
615 "edge crash",
616 "firefox slow",
617 "firefox crash",
618 "browser slow",
619 "browser crash",
620 "browser not",
621 "browser keeps"
622 ],
623 &[("browser_health", "Browser Health")]
624 );
625
626 add_if!(
627 &[
628 "screen flicker",
629 "display flicker",
630 "screen blink",
631 "monitor flicker",
632 "display artifact",
633 "screen glitch",
634 "black screen",
635 "screen goes black"
636 ],
637 &[
638 ("display_config", "Display Config"),
639 ("device_health", "Device Health"),
640 ("drivers", "Drivers")
641 ]
642 );
643 add_if!(
644 &[
645 "high disk",
646 "disk 100",
647 "disk usage",
648 "disk is full",
649 "disk almost full",
650 "disk at 100",
651 "storage full",
652 "no space left"
653 ],
654 &[
655 ("resource_load", "Resource Load"),
656 ("storage", "Storage"),
657 ("processes", "Processes"),
658 ("disk_health", "Disk Health")
659 ]
660 );
661 add_if!(
662 &[
663 "overheat",
664 "overheating",
665 "too hot",
666 "running hot",
667 "laptop hot",
668 "pc getting hot",
669 "temperature high",
670 "cpu temperature",
671 "thermal throttl",
672 "fan loud",
673 "fan noise",
674 "fans running",
675 "fan running",
676 "fans spinning",
677 "fan spinning",
678 "loud fan",
679 "fan always on",
680 "fan constantly",
681 "fan at max",
682 "fan at 100"
683 ],
684 &[
685 ("thermal", "Thermal"),
686 ("cpu_power", "CPU Power"),
687 ("overclocker", "GPU / Overclocker Telemetry")
688 ]
689 );
690 add_if!(
691 &[
692 "out of memory",
693 "low memory",
694 "ram full",
695 "running out of ram",
696 "memory full",
697 "ram usage high",
698 "ram usage",
699 "memory usage high",
700 "physical memory",
701 "ram almost",
702 "not enough memory",
703 "memory leak"
704 ],
705 &[
706 ("resource_load", "Resource Load"),
707 ("pagefile", "Page File"),
708 ("storage", "Storage"),
709 ("processes", "Processes")
710 ]
711 );
712 add_if!(
713 &[
714 "gpu",
715 "graphics",
716 "game slow",
717 "games not",
718 "gaming performance",
719 "fps low",
720 "fps drop",
721 "game crash",
722 "video card",
723 "graphics card",
724 "stuttering"
725 ],
726 &[
727 ("overclocker", "GPU / Overclocker Telemetry"),
728 ("thermal", "Thermal"),
729 ("resource_load", "Resource Load"),
730 ("drivers", "Drivers")
731 ]
732 );
733 add_if!(
734 &[
735 "nvlddmkm",
736 "amdkmdag",
737 "tdr failure",
738 "video_tdr_failure",
739 "gpu driver crash",
740 "gpu driver stopped",
741 "gpu hang",
742 "display adapter error"
743 ],
744 &[
745 ("device_health", "Device Health"),
746 ("drivers", "Drivers"),
747 ("recent_crashes", "Recent Crashes")
748 ]
749 );
750 add_if!(
751 &[
752 "startup slow",
753 "boot slow",
754 "slow boot",
755 "takes long to start",
756 "slow to start",
757 "long boot"
758 ],
759 &[
760 ("startup_items", "Startup Items"),
761 ("resource_load", "Resource Load"),
762 ("services", "Services")
763 ]
764 );
765 add_if!(
766 &[
767 "no sound",
768 "audio not",
769 "sound not",
770 "speaker not",
771 "microphone not",
772 "mic not",
773 "audio stopped",
774 "crackling audio",
775 "distorted sound",
776 "no audio"
777 ],
778 &[("audio", "Audio")]
779 );
780 add_if!(
781 &[
782 "install app",
783 "install fail",
784 "can't install",
785 "installation fail",
786 "app won't install",
787 "setup fail",
788 "winget fail",
789 "store install"
790 ],
791 &[
792 ("installer_health", "Installer Health"),
793 ("pending_reboot", "Pending Reboot")
794 ]
795 );
796 add_if!(
797 &[
798 "usb",
799 "usb not recognized",
800 "usb not working",
801 "device not recognized",
802 "device manager error",
803 "device manager",
804 "unknown device",
805 "code 43",
806 "code 10",
807 "yellow bang",
808 "pnp error",
809 "driver missing"
810 ],
811 &[
812 ("device_health", "Device Health"),
813 ("drivers", "Drivers"),
814 ("usb_history", "USB History")
815 ]
816 );
817 add_if!(
818 &[
819 "no wifi",
820 "no wi-fi",
821 "can't find wifi",
822 "can't find wi-fi",
823 "wifi not showing",
824 "no wireless networks",
825 "no networks found",
826 "wifi adapter",
827 "wireless adapter"
828 ],
829 &[
830 ("wifi", "Wi-Fi"),
831 ("device_health", "Device Health"),
832 ("drivers", "Drivers")
833 ]
834 );
835 add_if!(
836 &[
837 "network share",
838 "shared folder",
839 "mapped drive",
840 "unc path",
841 "can't access share",
842 "network drive",
843 "smb share",
844 "\\\\server",
845 "file share"
846 ],
847 &[
848 ("share_access", "Share Access"),
849 ("connectivity", "Connectivity"),
850 ("shares", "Shares")
851 ]
852 );
853 add_if!(
854 &[
855 "microsoft store",
856 "windows store",
857 "appx",
858 "store not",
859 "store won't",
860 "store app",
861 "wsreset"
862 ],
863 &[
864 ("installer_health", "Installer Health"),
865 ("pending_reboot", "Pending Reboot")
866 ]
867 );
868 add_if!(
869 &[
870 "sleep",
871 "hibernate",
872 "won't wake",
873 "won't sleep",
874 "stuck after sleep",
875 "wake from sleep",
876 "keeps waking",
877 "fast startup",
878 "power issue",
879 "power problem"
880 ],
881 &[
882 ("log_check", "Event Log"),
883 ("startup_items", "Startup Items"),
884 ("services", "Services")
885 ]
886 );
887 add_if!(
888 &[
889 "keyboard",
890 "mouse not",
891 "touchpad",
892 "trackpad",
893 "input device",
894 "keyboard not",
895 "mouse frozen",
896 "keyboard frozen"
897 ],
898 &[
899 ("peripherals", "Peripherals"),
900 ("device_health", "Device Health")
901 ]
902 );
903 add_if!(
904 &[
905 "bandwidth",
906 "high network",
907 "network usage",
908 "using all",
909 "slow internet",
910 "data usage",
911 "upload slow",
912 "download slow",
913 "network slow"
914 ],
915 &[
916 ("network_stats", "Network Stats"),
917 ("connections", "Active Connections"),
918 ("processes", "Processes")
919 ]
920 );
921 add_if!(
922 &[
923 "wifi disconnects",
924 "wifi drops",
925 "wifi keeps dropping",
926 "wifi keeps disconnecting",
927 "internet keeps cutting",
928 "connection drops",
929 "wifi intermittent",
930 "wifi unstable"
931 ],
932 &[
933 ("wifi", "Wi-Fi"),
934 ("network_adapter", "Network Adapter"),
935 ("connectivity", "Connectivity")
936 ]
937 );
938 add_if!(
939 &[
940 "msmpeng",
941 "antimalware service",
942 "defender high cpu",
943 "wdnissvc",
944 "defender scan"
945 ],
946 &[
947 ("resource_load", "Resource Load"),
948 ("security", "Security"),
949 ("processes", "Processes")
950 ]
951 );
952 add_if!(
953 &[
954 "monitor not detected",
955 "second monitor",
956 "hdmi not working",
957 "hdmi not detected",
958 "displayport not",
959 "external monitor",
960 "external display",
961 "no signal on monitor",
962 "no signal on screen",
963 "monitor not showing",
964 "screen not detected",
965 "extend display",
966 "duplicate display",
967 "second screen"
968 ],
969 &[
970 ("display_config", "Display Config"),
971 ("device_health", "Device Health"),
972 ("drivers", "Drivers")
973 ]
974 );
975 add_if!(
976 &[
977 "explorer.exe crash",
978 "windows explorer crash",
979 "file explorer crash",
980 "file explorer",
981 "explorer crash",
982 "desktop icons disappeared",
983 "desktop disappeared",
984 "taskbar disappeared",
985 "taskbar not responding",
986 "taskbar missing",
987 "taskbar gone",
988 "start menu crashed",
989 "start menu not working",
990 "start menu broken",
991 "desktop froze"
992 ],
993 &[("processes", "Processes"), ("log_check", "Event Log")]
994 );
995 add_if!(
996 &[
997 "access denied",
998 "access is denied",
999 "permission denied",
1000 "you don't have permission",
1001 "cannot access this folder",
1002 "file access denied",
1003 "folder access denied",
1004 "unauthorized access"
1005 ],
1006 &[("user_accounts", "User Accounts"), ("shares", "Shares")]
1007 );
1008
1009 add_if!(
1011 &[
1012 "ip address conflict",
1013 "no ip address",
1014 "can't get ip",
1015 "apipa",
1016 "169.254",
1017 "ipv4",
1018 "dhcp not",
1019 "dhcp fail",
1020 "dhcp server",
1021 "dhcp lease",
1022 "ip not assigned",
1023 "no dhcp"
1024 ],
1025 &[
1026 ("dhcp", "DHCP Lease"),
1027 ("ip_config", "IP Config"),
1028 ("connectivity", "Connectivity")
1029 ]
1030 );
1031 add_if!(
1032 &[
1033 "ipv6",
1034 "ipv6 not",
1035 "ipv6 fail",
1036 "no ipv6",
1037 "ipv6 issue",
1038 "ipv6 address",
1039 "slaac",
1040 "dhcpv6"
1041 ],
1042 &[("ipv6", "IPv6"), ("connectivity", "Connectivity")]
1043 );
1044 add_if!(
1045 &[
1046 "mtu",
1047 "packet fragmentation",
1048 "pmtu",
1049 "fragmented packet",
1050 "mtu mismatch"
1051 ],
1052 &[("mtu", "MTU"), ("connectivity", "Connectivity")]
1053 );
1054 add_if!(
1056 &[
1057 "certificate expired",
1058 "certificate is expired",
1059 "certificate error",
1060 "ssl error",
1061 "cert expired",
1062 "certificate not trusted",
1063 "ssl certificate",
1064 "tls error",
1065 "tls handshake",
1066 "tls fail",
1067 "certificate warning"
1068 ],
1069 &[("certificates", "Certificates")]
1070 );
1071 add_if!(
1072 &[
1073 "tpm not",
1074 "no tpm",
1075 "tpm missing",
1076 "tpm error",
1077 "tpm chip",
1078 "secure boot",
1079 "uefi secure",
1080 "bitlocker needs tpm"
1081 ],
1082 &[("tpm", "TPM / Secure Boot")]
1083 );
1084 add_if!(
1085 &[
1086 "smb1",
1087 "smb version 1",
1088 "ntlmv1",
1089 "ntlm level",
1090 "ntlm authentication level",
1091 "lmcompatibility",
1092 "lan manager"
1093 ],
1094 &[
1095 ("shares", "SMB Shares"),
1096 ("local_security_policy", "Local Security Policy")
1097 ]
1098 );
1099 add_if!(
1101 &[
1102 "pagefile",
1103 "page file",
1104 "hiberfil",
1105 "hiberfile",
1106 "virtual memory file",
1107 "virtual memory low",
1108 "memory commit",
1109 "commit charge",
1110 "swapfile.sys"
1111 ],
1112 &[("pagefile", "Page File"), ("storage", "Storage")]
1113 );
1114 add_if!(
1115 &[
1116 "windows search",
1117 "search index",
1118 "search indexing",
1119 "wsearch",
1120 "search eating",
1121 "cortana search",
1122 "cortana not",
1123 "search bar not"
1124 ],
1125 &[("search_index", "Search Index"), ("storage", "Storage")]
1126 );
1127 add_if!(
1129 &[
1130 "wmi not",
1131 "wmi is",
1132 "wmi error",
1133 "wmi corrupt",
1134 "wmi fail",
1135 "wmi broken",
1136 "winmgmt"
1137 ],
1138 &[("wmi_health", "WMI Health")]
1139 );
1140 add_if!(
1141 &[
1142 "event log full",
1143 "event log error",
1144 "event viewer",
1145 "event log cleared",
1146 "audit log"
1147 ],
1148 &[("log_check", "Event Log"), ("services", "Services")]
1149 );
1150 add_if!(
1151 &[
1152 "ctrl+alt+del",
1153 "login screen stuck",
1154 "stuck at login",
1155 "stuck on login",
1156 "login loop",
1157 "sign-in loop",
1158 "sign in loop",
1159 "reboot loop on login",
1160 "stuck on lock screen",
1161 "winlogon",
1162 "sign in screen stuck",
1163 "can't get past login",
1164 "welcome screen stuck"
1165 ],
1166 &[
1167 ("sign_in", "Sign-In / Windows Hello"),
1168 ("services", "Services")
1169 ]
1170 );
1171 add_if!(
1172 &[
1173 "reset windows",
1174 "can't reset",
1175 "windows reset",
1176 "reinstall windows",
1177 "factory reset windows",
1178 "repair windows install",
1179 "windows recovery"
1180 ],
1181 &[
1182 ("installer_health", "Installer Health"),
1183 ("pending_reboot", "Pending Reboot"),
1184 ("integrity", "Integrity")
1185 ]
1186 );
1187 add_if!(
1188 &[
1189 "scheduled task",
1190 "task scheduler",
1191 "task not running",
1192 "scheduled job",
1193 "task failed"
1194 ],
1195 &[
1196 ("scheduled_tasks", "Scheduled Tasks"),
1197 ("log_check", "Event Log")
1198 ]
1199 );
1200 add_if!(
1201 &[
1202 "ssh connection",
1203 "ssh refused",
1204 "ssh timeout",
1205 "openssh",
1206 "ssh not working",
1207 "ssh key",
1208 "sshd",
1209 "known_hosts",
1210 "ssh host key",
1211 "ssh permissions"
1212 ],
1213 &[
1214 ("ssh", "SSH"),
1215 ("services", "Services"),
1216 ("firewall_rules", "Firewall Rules"),
1217 ("connectivity", "Connectivity")
1218 ]
1219 );
1220 add_if!(
1221 &[
1222 "git credential",
1223 "git push denied",
1224 "git auth",
1225 "github auth",
1226 "git permission",
1227 "git clone failed",
1228 "git identity",
1229 "git signing",
1230 "git commit sign",
1231 "commit signing",
1232 "fails sign",
1233 "gpg signing",
1234 "gpg key git",
1235 "git gpg"
1236 ],
1237 &[("git_config", "Git Config"), ("credentials", "Credentials")]
1238 );
1239 add_if!(
1240 &[
1241 "not found in path",
1242 "command not found",
1243 "python not found",
1244 "node not found",
1245 "node.js version",
1246 "node.js not",
1247 "cargo not",
1248 "pip not",
1249 "pip install",
1250 "conda environment",
1251 "conda env",
1252 "path issue",
1253 "missing from path",
1254 "env var not set",
1255 "toolchain not found"
1256 ],
1257 &[
1258 ("toolchains", "Toolchains"),
1259 ("env", "Environment Vars"),
1260 ("path", "PATH")
1261 ]
1262 );
1263 add_if!(
1264 &[
1265 "version conflict",
1266 "node version",
1267 "python version conflict",
1268 "dev conflict",
1269 "package manager conflict",
1270 "conda shadow",
1271 "nvm conflict",
1272 "pyenv conflict"
1273 ],
1274 &[
1275 ("dev_conflicts", "Dev Conflicts"),
1276 ("toolchains", "Toolchains")
1277 ]
1278 );
1279 add_if!(
1280 &[
1281 "group policy",
1282 "gpo ",
1283 "gpo:",
1284 "group policy error",
1285 "policy enforcement"
1286 ],
1287 &[("gpo", "Group Policy"), ("domain_health", "Domain Health")]
1288 );
1289 add_if!(
1290 &[
1291 "windows license",
1292 "not activated",
1293 "activate windows",
1294 "activation error",
1295 "product key",
1296 "license expired",
1297 "license invalid",
1298 "windows isn't activated",
1299 "not genuine",
1300 "windows is not genuine",
1301 "genuine check"
1302 ],
1303 &[("activation", "Windows Activation")]
1304 );
1305 add_if!(
1306 &[
1307 "bitlocker",
1308 "recovery key",
1309 "bitlocker locked",
1310 "drive encryption",
1311 "encrypted drive",
1312 "decryption failed"
1313 ],
1314 &[("bitlocker", "BitLocker"), ("tpm", "TPM / Secure Boot")]
1315 );
1316 add_if!(
1317 &[
1318 "can't join domain",
1319 "domain join",
1320 "domain controller",
1321 "domain not reachable",
1322 "kerberos",
1323 "ldap error",
1324 "dc unreachable",
1325 "gpo not applying",
1326 "group policy not",
1327 "group policy error",
1328 "policy not applied"
1329 ],
1330 &[
1331 ("domain_health", "Domain Health"),
1332 ("gpo", "Group Policy"),
1333 ("identity_auth", "Identity & Auth")
1334 ]
1335 );
1336 add_if!(
1337 &[
1338 "hyper-v",
1339 "hyperv",
1340 "vm won't start",
1341 "virtual machine won't",
1342 "vm not starting",
1343 "virtual machine error",
1344 "vm crash",
1345 "vm network"
1346 ],
1347 &[
1348 ("hyperv", "Hyper-V"),
1349 ("storage", "Storage"),
1350 ("disk_health", "Disk Health")
1351 ]
1352 );
1353 add_if!(
1354 &[
1355 "wsl",
1356 "wsl2",
1357 "linux subsystem",
1358 "wsl not",
1359 "wsl won't",
1360 "linux on windows",
1361 "bash on windows"
1362 ],
1363 &[
1364 ("wsl", "WSL"),
1365 ("wsl_filesystems", "WSL Filesystems"),
1366 ("connectivity", "Connectivity"),
1367 ("dns_servers", "DNS Servers")
1368 ]
1369 );
1370 add_if!(
1371 &[
1372 "docker",
1373 "container",
1374 "docker compose",
1375 "docker daemon",
1376 "docker not",
1377 "docker won't"
1378 ],
1379 &[
1380 ("docker", "Docker"),
1381 ("docker_filesystems", "Docker Filesystems"),
1382 ("connectivity", "Connectivity")
1383 ]
1384 );
1385 add_if!(
1386 &[
1387 "restarts randomly",
1388 "keeps restarting",
1389 "random restart",
1390 "random reboot",
1391 "reboot randomly",
1392 "reboots itself",
1393 "spontaneous restart",
1394 "unexpected reboot",
1395 "auto restart"
1396 ],
1397 &[
1398 ("recent_crashes", "Crash History"),
1399 ("log_check", "Event Log"),
1400 ("pending_reboot", "Pending Reboot")
1401 ]
1402 );
1403 add_if!(
1404 &[
1405 "disk filling up",
1406 "drive filling",
1407 "storage filling",
1408 "ssd filling",
1409 "getting full",
1410 "filling up fast",
1411 "recycle bin",
1412 "temp files growing",
1413 "disk growing"
1414 ],
1415 &[
1416 ("storage", "Storage"),
1417 ("disk_health", "Disk Health"),
1418 ("shadow_copies", "Shadow Copies")
1419 ]
1420 );
1421
1422 add_if!(
1423 &[
1424 "how much ram",
1425 "check my ram",
1426 "check ram",
1427 "my ram size",
1428 "show ram",
1429 "what cpu",
1430 "cpu model",
1431 "what processor",
1432 "ram size",
1433 "motherboard model",
1434 "system specs",
1435 "hardware specs",
1436 "bios version",
1437 "what gpu",
1438 "gpu model",
1439 "hardware info",
1440 "check specs"
1441 ],
1442 &[("hardware", "Hardware")]
1443 );
1444 add_if!(
1445 &[
1446 "windows version",
1447 "what version of windows",
1448 "windows 10 or 11",
1449 "os version",
1450 "build number",
1451 "windows build",
1452 "edition of windows",
1453 "which windows"
1454 ],
1455 &[("os_config", "OS Config")]
1456 );
1457 add_if!(
1458 &[
1459 "someone accessed",
1460 "unauthorized access",
1461 "who logged in",
1462 "remote access log",
1463 "logon sessions",
1464 "active sessions",
1465 "who is connected"
1466 ],
1467 &[
1468 ("sessions", "Logon Sessions"),
1469 ("security", "Security Posture"),
1470 ("log_check", "Event Log")
1471 ]
1472 );
1473 add_if!(
1474 &[
1475 "ethernet not",
1476 "ethernet port",
1477 "wired connection not",
1478 "wired network not",
1479 "nic not working",
1480 "network adapter not",
1481 "network card",
1482 "ethernet cable"
1483 ],
1484 &[
1485 ("network_adapter", "Network Adapter"),
1486 ("connectivity", "Connectivity"),
1487 ("device_health", "Device Health")
1488 ]
1489 );
1490 add_if!(
1491 &[
1492 "task manager",
1493 "process explorer",
1494 "what processes",
1495 "most resources",
1496 "using the most",
1497 "top processes",
1498 "resource hog"
1499 ],
1500 &[
1501 ("processes", "Processes"),
1502 ("resource_load", "Resource Load")
1503 ]
1504 );
1505 add_if!(
1506 &[
1507 "battery",
1508 "not charging",
1509 "battery drain",
1510 "battery low",
1511 "battery dead",
1512 "won't charge",
1513 "wont charge",
1514 "plugged in not charging",
1515 "power adapter",
1516 "ac adapter",
1517 "battery percentage"
1518 ],
1519 &[
1520 ("battery", "Battery"),
1521 ("thermal", "Thermal"),
1522 ("cpu_power", "CPU Power")
1523 ]
1524 );
1525 add_if!(
1526 &[
1527 "disk benchmark",
1528 "storage speed",
1529 "read write speed",
1530 "sequential read",
1531 "sequential write",
1532 "io throughput",
1533 "disk throughput",
1534 "disk performance",
1535 "drive speed test"
1536 ],
1537 &[("disk_benchmark", "Disk Benchmark")]
1538 );
1539 add_if!(
1540 &[
1541 "uac prompt",
1542 "uac not",
1543 "uac disabled",
1544 "user account control",
1545 "admin rights",
1546 "needs admin",
1547 "needs elevation",
1548 "run as administrator",
1549 "administrator permission"
1550 ],
1551 &[
1552 ("local_security_policy", "Local Security Policy"),
1553 ("user_accounts", "User Accounts")
1554 ]
1555 );
1556
1557 if topics.is_empty() {
1558 topics.push(("health_report", "System Health"));
1559 topics.push(("log_check", "Event Log"));
1560 }
1561 topics
1562}
1563
1564pub fn fix_plan_topics(issue: &str) -> Vec<(&'static str, &'static str)> {
1565 topics_for_issue(issue)
1566}
1567
1568pub struct AutoFix {
1573 pub label: &'static str,
1574 pub cmd: &'static str,
1575 pub verify_topic: Option<&'static str>,
1576 pub verify_gone: Option<&'static str>,
1577 pub include_in_sweep: bool,
1578}
1579
1580struct AutoCmdAc {
1581 ac: aho_corasick::AhoCorasick,
1582 entries: Vec<AutoFix>,
1583}
1584
1585static AUTO_CMD_AC: std::sync::OnceLock<AutoCmdAc> = std::sync::OnceLock::new();
1586
1587fn auto_cmd_ac() -> &'static AutoCmdAc {
1588 AUTO_CMD_AC.get_or_init(|| {
1589 #[allow(clippy::type_complexity)]
1591 const SAFE: &[(&str, &str, &str, Option<&str>, Option<&str>, bool)] = &[
1592 (
1593 "dns: failed",
1594 "Flush DNS cache",
1595 "ipconfig /flushdns",
1596 Some("connectivity"),
1597 Some("dns: failed"),
1598 true,
1599 ),
1600 (
1601 "dns resolution: failed",
1602 "Flush DNS cache",
1603 "ipconfig /flushdns",
1604 Some("connectivity"),
1605 Some("dns: failed"),
1606 false, ),
1608 (
1609 "wsearch",
1610 "Restart Windows Search",
1611 "powershell -Command \"Restart-Service WSearch -ErrorAction SilentlyContinue\"",
1612 Some("search_index"),
1613 Some("stopped"),
1614 true,
1615 ),
1616 (
1617 "windows search",
1618 "Restart Windows Search",
1619 "powershell -Command \"Restart-Service WSearch -ErrorAction SilentlyContinue\"",
1620 Some("search_index"),
1621 Some("stopped"),
1622 false, ),
1624 (
1625 "spooler",
1626 "Restart Print Spooler",
1627 "powershell -Command \"Restart-Service Spooler -Force\"",
1628 Some("printers"),
1629 Some("offline"),
1630 true,
1631 ),
1632 (
1633 "print spooler",
1634 "Restart Print Spooler",
1635 "powershell -Command \"Restart-Service Spooler -Force\"",
1636 Some("printers"),
1637 Some("offline"),
1638 false, ),
1640 (
1641 "ntp source unreachable",
1642 "Resync system clock",
1643 "w32tm /resync /force",
1644 Some("ntp"),
1645 Some("failed"),
1646 true,
1647 ),
1648 (
1649 "time sync failed",
1650 "Resync system clock",
1651 "w32tm /resync /force",
1652 Some("ntp"),
1653 Some("failed"),
1654 false, ),
1656 (
1657 "bits",
1658 "Restart BITS service",
1659 "powershell -Command \"Restart-Service BITS -Force\"",
1660 None,
1661 None,
1662 false, ),
1664 (
1665 "wuauserv",
1666 "Restart Windows Update service",
1667 "powershell -Command \"Restart-Service wuauserv -Force\"",
1668 None,
1669 None,
1670 false,
1671 ),
1672 (
1673 "windows update service",
1674 "Restart Windows Update service",
1675 "powershell -Command \"Restart-Service wuauserv -Force\"",
1676 None,
1677 None,
1678 false, ),
1680 (
1681 "audiosrv",
1682 "Restart Audio service",
1683 "powershell -Command \"Restart-Service Audiosrv -Force\"",
1684 Some("audio"),
1685 Some("not running"),
1686 true,
1687 ),
1688 (
1689 "windows audio",
1690 "Restart Audio service",
1691 "powershell -Command \"Restart-Service Audiosrv -Force\"",
1692 Some("audio"),
1693 Some("not running"),
1694 false, ),
1696 (
1697 "low disk",
1698 "Empty Recycle Bin",
1699 "powershell -Command \"Clear-RecycleBin -Force -ErrorAction SilentlyContinue\"",
1700 None,
1701 None,
1702 true, ),
1704 (
1705 "free up space",
1706 "Empty Recycle Bin",
1707 "powershell -Command \"Clear-RecycleBin -Force -ErrorAction SilentlyContinue\"",
1708 None,
1709 None,
1710 false, ),
1712 (
1714 "teams cache",
1715 "Clear Teams cache",
1716 "powershell -Command \"Get-Process ms-teams,Teams -ErrorAction SilentlyContinue | Stop-Process -Force; Remove-Item \\\"$env:APPDATA\\\\Microsoft\\\\Teams\\\\Cache\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue; Remove-Item \\\"$env:LOCALAPPDATA\\\\Packages\\\\MSTeams_8wekyb3d8bbwe\\\\LocalCache\\\\Microsoft\\\\MSTeams\\\\EBWebView\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1717 Some("teams"),
1718 Some("cache:"),
1719 true,
1720 ),
1721 (
1722 "msteams",
1723 "Clear Teams cache",
1724 "powershell -Command \"Get-Process ms-teams,Teams -ErrorAction SilentlyContinue | Stop-Process -Force; Remove-Item \\\"$env:APPDATA\\\\Microsoft\\\\Teams\\\\Cache\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue; Remove-Item \\\"$env:LOCALAPPDATA\\\\Packages\\\\MSTeams_8wekyb3d8bbwe\\\\LocalCache\\\\Microsoft\\\\MSTeams\\\\EBWebView\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1725 Some("teams"),
1726 Some("cache:"),
1727 false, ),
1729 (
1731 "bluetooth service",
1732 "Restart Bluetooth service",
1733 "powershell -Command \"Restart-Service bthserv -Force -ErrorAction SilentlyContinue\"",
1734 Some("bluetooth"),
1735 Some("not running"),
1736 true,
1737 ),
1738 (
1739 "bthserv",
1740 "Restart Bluetooth service",
1741 "powershell -Command \"Restart-Service bthserv -Force -ErrorAction SilentlyContinue\"",
1742 Some("bluetooth"),
1743 Some("not running"),
1744 false, ),
1746 (
1748 "dhcp lease expired",
1749 "Renew DHCP lease",
1750 "ipconfig /release && ipconfig /renew",
1751 Some("dhcp"),
1752 Some("expired"),
1753 false,
1754 ),
1755 (
1756 "lease expires",
1757 "Renew DHCP lease",
1758 "ipconfig /release && ipconfig /renew",
1759 Some("dhcp"),
1760 Some("expired"),
1761 false, ),
1763 (
1765 "dnscache",
1766 "Restart DNS Client service",
1767 "powershell -Command \"Restart-Service Dnscache -Force -ErrorAction SilentlyContinue\"",
1768 Some("connectivity"),
1769 Some("dns: failed"),
1770 false,
1771 ),
1772 (
1774 "onedrive not running",
1775 "Start OneDrive",
1776 "powershell -Command \"Start-Process \\\"$env:LOCALAPPDATA\\\\Microsoft\\\\OneDrive\\\\OneDrive.exe\\\" -ErrorAction SilentlyContinue\"",
1777 Some("onedrive"),
1778 Some("not running"),
1779 true,
1780 ),
1781 (
1783 "wmi repository",
1784 "Restart WMI service",
1785 "powershell -Command \"Restart-Service winmgmt -Force\"",
1786 Some("wmi_health"),
1787 Some("corrupt"),
1788 false,
1789 ),
1790 (
1791 "winmgmt",
1792 "Restart WMI service",
1793 "powershell -Command \"Restart-Service winmgmt -Force\"",
1794 Some("wmi_health"),
1795 Some("corrupt"),
1796 false, ),
1798 (
1800 "unidentified network",
1801 "Restart Network Location Awareness",
1802 "powershell -Command \"Restart-Service NlaSvc -Force -ErrorAction SilentlyContinue\"",
1803 Some("network_profile"),
1804 Some("unidentified"),
1805 true,
1806 ),
1807 (
1809 "temp folder",
1810 "Clear Windows Temp folder",
1811 "powershell -Command \"Remove-Item \\\"$env:TEMP\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1812 None,
1813 None,
1814 true, ),
1816 (
1817 "temporary files",
1818 "Clear Windows Temp folder",
1819 "powershell -Command \"Remove-Item \\\"$env:TEMP\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1820 None,
1821 None,
1822 false, ),
1824 (
1826 "firewall: off",
1827 "Restart Windows Firewall",
1828 "powershell -Command \"Restart-Service MpsSvc -Force -ErrorAction SilentlyContinue\"",
1829 Some("security"),
1830 Some("firewall: off"),
1831 true,
1832 ),
1833 (
1834 "firewall profile: disabled",
1835 "Restart Windows Firewall",
1836 "powershell -Command \"Restart-Service MpsSvc -Force -ErrorAction SilentlyContinue\"",
1837 Some("security"),
1838 Some("firewall: off"),
1839 false, ),
1841 (
1843 "winsock",
1844 "Reset TCP/IP stack",
1845 "netsh int ip reset && netsh winsock reset",
1846 Some("connectivity"),
1847 Some("unreachable"),
1848 false,
1849 ),
1850 (
1852 "termservice",
1853 "Restart Remote Desktop Services",
1854 "powershell -Command \"Restart-Service TermService -Force -ErrorAction SilentlyContinue\"",
1855 Some("rdp"),
1856 Some("stopped"),
1857 false,
1858 ),
1859 (
1861 "wlansvc",
1862 "Restart WLAN AutoConfig service",
1863 "powershell -Command \"Restart-Service Wlansvc -Force -ErrorAction SilentlyContinue\"",
1864 Some("wifi"),
1865 Some("stopped"),
1866 false,
1867 ),
1868 (
1870 "cryptsvc",
1871 "Restart Cryptographic Services",
1872 "powershell -Command \"Restart-Service CryptSvc -Force -ErrorAction SilentlyContinue\"",
1873 Some("identity_auth"),
1874 Some("cryptsvc"),
1875 false,
1876 ),
1877 (
1879 "microsoft.windowsstore | status: missing",
1880 "Reset Microsoft Store cache",
1881 "%SystemRoot%\\System32\\wsreset.exe",
1882 None,
1883 None,
1884 false, ),
1886 (
1888 "browser slow",
1889 "Clear Microsoft Edge cache",
1890 "powershell -Command \"Remove-Item \\\"$env:LOCALAPPDATA\\\\Microsoft\\\\Edge\\\\User Data\\\\Default\\\\Cache\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1891 None,
1892 None,
1893 false, ),
1895 (
1896 "browser crashing",
1897 "Clear Microsoft Edge cache",
1898 "powershell -Command \"Remove-Item \\\"$env:LOCALAPPDATA\\\\Microsoft\\\\Edge\\\\User Data\\\\Default\\\\Cache\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue\"",
1899 None,
1900 None,
1901 false, ),
1903 (
1905 "rasman",
1906 "Restart Remote Access Connection Manager (VPN)",
1907 "powershell -Command \"Restart-Service RasMan -Force -ErrorAction SilentlyContinue\"",
1908 Some("vpn"),
1909 Some("not running"),
1910 false, ),
1912 (
1913 "vpn adapter detected",
1914 "Restart Remote Access Connection Manager (VPN)",
1915 "powershell -Command \"Restart-Service RasMan -Force -ErrorAction SilentlyContinue\"",
1916 Some("vpn"),
1917 Some("not running"),
1918 false, ),
1920 (
1922 "update stuck downloading",
1923 "Reset Windows Update components",
1924 "powershell -Command \"net stop wuauserv; net stop bits; Remove-Item \\\"C:\\\\Windows\\\\SoftwareDistribution\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue; net start wuauserv; net start bits\"",
1925 None,
1926 None,
1927 false, ),
1929 (
1930 "update error 0x",
1931 "Reset Windows Update components",
1932 "powershell -Command \"net stop wuauserv; net stop bits; Remove-Item \\\"C:\\\\Windows\\\\SoftwareDistribution\\\\*\\\" -Recurse -Force -ErrorAction SilentlyContinue; net start wuauserv; net start bits\"",
1933 None,
1934 None,
1935 false, ),
1937 (
1939 "remote desktop disabled",
1940 "Enable Remote Desktop",
1941 "powershell -Command \"Set-ItemProperty -Path 'HKLM:\\System\\CurrentControlSet\\Control\\Terminal Server' -Name fDenyTSConnections -Value 0; Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue\"",
1942 Some("rdp"),
1943 Some("disabled"),
1944 false,
1945 ),
1946 (
1948 "tokenbroker | status: stopped",
1949 "Restart M365 Token Broker",
1950 "powershell -Command \"Restart-Service TokenBroker -Force -ErrorAction SilentlyContinue\"",
1951 Some("identity_auth"),
1952 Some("tokenbroker | status: stopped"),
1953 true,
1954 ),
1955 (
1956 "token broker: not running",
1957 "Restart M365 Token Broker",
1958 "powershell -Command \"Restart-Service TokenBroker -Force -ErrorAction SilentlyContinue\"",
1959 Some("identity_auth"),
1960 Some("tokenbroker | status: stopped"),
1961 false, ),
1963 (
1965 "msiserver | status: stopped | starttype: disabled",
1966 "Re-enable Windows Installer service",
1967 "powershell -Command \"Set-Service msiserver -StartupType Manual; Start-Service msiserver -ErrorAction SilentlyContinue\"",
1968 Some("installer_health"),
1969 Some("msiserver | status: stopped"),
1970 false,
1971 ),
1972 (
1974 "delivery optimization",
1975 "Clear Delivery Optimization cache",
1976 "powershell -Command \"Delete-DeliveryOptimizationCache -Force -ErrorAction SilentlyContinue\"",
1977 None,
1978 None,
1979 false,
1980 ),
1981 (
1983 "sysmain",
1984 "Restart SysMain service",
1985 "powershell -Command \"Restart-Service SysMain -ErrorAction SilentlyContinue\"",
1986 None,
1987 None,
1988 false,
1989 ),
1990 ];
1991 let mut patterns: Vec<&str> = Vec::with_capacity(SAFE.len());
1992 let mut entries: Vec<AutoFix> = Vec::with_capacity(SAFE.len());
1993 for &(trigger, label, cmd, verify_topic, verify_gone, include_in_sweep) in SAFE {
1994 patterns.push(trigger);
1995 entries.push(AutoFix { label, cmd, verify_topic, verify_gone, include_in_sweep });
1996 }
1997 AutoCmdAc {
1998 ac: aho_corasick::AhoCorasick::new(&patterns).expect("valid patterns"),
1999 entries,
2000 }
2001 })
2002}
2003
2004pub fn fix_plan_auto_commands(combined_output: &str) -> Vec<&'static AutoFix> {
2005 let lower = combined_output.to_ascii_lowercase();
2006 let state = auto_cmd_ac();
2007 let mut seen_labels = std::collections::HashSet::new();
2008 let mut result: Vec<&'static AutoFix> = Vec::new();
2009 for mat in state.ac.find_iter(&lower) {
2010 let fix = &state.entries[mat.pattern().as_usize()];
2011 if seen_labels.insert(fix.label) {
2012 result.push(fix);
2013 }
2014 }
2015 result
2016}
2017
2018pub fn sweep_auto_fixes() -> Vec<&'static AutoFix> {
2021 let state = auto_cmd_ac();
2022 let mut seen = std::collections::HashSet::new();
2023 state
2024 .entries
2025 .iter()
2026 .filter(|f| f.include_in_sweep && seen.insert(f.label))
2027 .collect()
2028}
2029
2030pub fn recipe_title_to_fix_arg(title: &str) -> Option<&'static str> {
2033 match title {
2034 t if t.contains("disk space") || t.contains("Low disk") => Some("disk full"),
2035 t if t.contains("Drive health") || t.contains("failure") => Some("disk health warning"),
2036 t if t.contains("Restart required") => Some("restart required"),
2037 t if t.contains("event log errors") => Some("Windows errors in event log"),
2038 t if t.contains("service not running") => Some("critical service stopped"),
2039 t if t.contains("No internet") => Some("can't connect to internet"),
2040 t if t.contains("latency") => Some("high network latency"),
2041 t if t.contains("memory usage") => Some("high RAM usage"),
2042 t if t.contains("running hot") || t.contains("CPU") => Some("CPU running hot"),
2043 t if t.contains("security protection") => Some("Windows Defender disabled"),
2044 t if t.contains("Threat detected") => Some("virus or malware detected"),
2045 t if t.contains("updates pending") => Some("Windows updates pending"),
2046 t if t.contains("Hardware device error") => Some("hardware device error"),
2047 t if t.contains("No backup") => Some("no backup configured"),
2048 t if t.contains("SMB1") => Some("SMB1 security risk"),
2049 t if t.contains("encryption not enabled") => Some("BitLocker not enabled"),
2050 t if t.contains("DNS resolution") => Some("DNS not resolving"),
2051 t if t.contains("Application crashing") => Some("app crashing repeatedly"),
2052 t if t.contains("Wi-Fi signal weak") => Some("weak Wi-Fi signal"),
2053 t if t.contains("clock not synchronizing") => Some("clock out of sync"),
2054 t if t.contains("system file corruption") => Some("Windows system files corrupt"),
2055 t if t.contains("Service failed") => Some("service stopped unexpectedly"),
2056 t if t.contains("Remote Desktop") => Some("RDP disabled or blocked"),
2057 t if t.contains("Windows Update service") => Some("Windows Update broken"),
2058 t if t.contains("Teams cache") => Some("Teams not working"),
2059 t if t.contains("authentication broker") => Some("Microsoft 365 sign-in broken"),
2060 t if t.contains("WMI repository") => Some("WMI errors"),
2061 t if t.contains("Windows not activated") => Some("Windows not activated"),
2062 t if t.contains("Windows Search") => Some("Windows search not finding files"),
2063 t if t.contains("OneDrive not syncing") => Some("OneDrive not syncing"),
2064 t if t.contains("Printer offline") => Some("printer offline or stuck"),
2065 t if t.contains("Outlook mail profile") => Some("Outlook not opening"),
2066 t if t.contains("PrintNightmare") => Some("PrintNightmare not mitigated"),
2067 t if t.contains("Temp folder") => Some("disk full"),
2068 t if t.contains("Windows Firewall") => Some("Windows Firewall stopped"),
2069 t if t.contains("TCP/IP") && t.contains("stack") => {
2070 Some("network not working after update")
2071 }
2072 t if t.contains("Remote Desktop Services") => Some("RDP not responding"),
2073 t if t.contains("WLAN AutoConfig") => Some("WiFi service stopped"),
2074 t if t.contains("Cryptographic Services") => Some("certificates not loading"),
2075 t if t.contains("No audio") => Some("no sound"),
2076 t if t.contains("Bluetooth not working") => Some("Bluetooth not connecting"),
2077 t if t.contains("App installation failing") => Some("app install not working"),
2078 t if t.contains("Blue screen") || t.contains("BSOD") => Some("BSOD or blue screen"),
2079 t if t.contains("Camera") && t.contains("webcam") => Some("camera not working"),
2080 t if t.contains("VPN not connecting") => Some("VPN not connecting"),
2081 t if t.contains("Screen flickering") => Some("screen flickering"),
2082 t if t.contains("Microphone not working") => Some("microphone not working"),
2083 t if t.contains("Login") && t.contains("PIN") => Some("PIN not working"),
2084 t if t.contains("Disk at 100%") => Some("disk at 100%"),
2085 t if t.contains("USB device not recognized") => Some("USB device not working"),
2086 t if t.contains("No Wi-Fi networks visible") => Some("no Wi-Fi networks showing"),
2087 t if t.contains("Network share") && t.contains("accessible") => {
2088 Some("network share not accessible")
2089 }
2090 t if t.contains("Microsoft Store") && t.contains("AppX") => {
2091 Some("Microsoft Store not working")
2092 }
2093 t if t.contains("sleep") && t.contains("hibernate") => Some("PC won't sleep or wake"),
2094 t if t.contains("Keyboard") && t.contains("mouse") => Some("keyboard not working"),
2095 t if t.contains("High network usage") => Some("high network usage"),
2096 t if t.contains("crackling") || t.contains("distortion") || t.contains("stuttering") => {
2097 Some("audio crackling")
2098 }
2099 t if t.contains("Browser") && (t.contains("slow") || t.contains("crashing")) => {
2100 Some("browser slow or crashing")
2101 }
2102 t if t.contains("Antimalware Service Executable") || t.contains("MsMpEng") => {
2103 Some("Defender high CPU")
2104 }
2105 t if t.contains("External monitor") || t.contains("no signal") => {
2106 Some("external monitor not detected")
2107 }
2108 t if t.contains("Explorer") && t.contains("crashed") => Some("desktop or taskbar crashed"),
2109 t if t.contains("Visual C++") => Some("Visual C++ runtime missing"),
2110 t if t.contains("Certificate expiring") => Some("certificate expiring soon"),
2111 t if t.contains("Page file not configured") => Some("page file not configured"),
2112 t if t.contains("startup is slow") || t.contains("long time to boot") => {
2113 Some("slow startup")
2114 }
2115 t if t.contains("Update stuck") => Some("Windows Update stuck"),
2116 t if t.contains("GPU") && t.contains("driver crash") => Some("GPU driver crash"),
2117 t if t.contains("Access denied") || t.contains("permission error") => {
2118 Some("access denied to file or folder")
2119 }
2120 t if t.contains("Wi-Fi keeps disconnecting") || t.contains("dropping intermittently") => {
2121 Some("Wi-Fi keeps dropping")
2122 }
2123 t if t.contains("Application crash history") => Some("app keeps crashing"),
2124 t if t.contains("Outlook add-in") || (t.contains("Outlook") && t.contains("OST")) => {
2125 Some("Outlook crashing or slow")
2126 }
2127 _ => None,
2128 }
2129}
2130
2131pub fn suggest_fix_commands(content: &str) -> Vec<String> {
2135 let recipes = crate::agent::fix_recipes::match_recipes(content);
2136 let mut seen = std::collections::HashSet::new();
2137 let mut suggestions: Vec<String> = Vec::new();
2138 for recipe in recipes {
2139 if recipe.severity == "MONITOR" {
2140 continue;
2141 }
2142 if let Some(arg) = recipe_title_to_fix_arg(recipe.title) {
2143 if seen.insert(arg) {
2144 suggestions.push(format!(" hematite --fix \"{}\"", arg));
2145 }
2146 }
2147 }
2148 suggestions
2149}
2150
2151pub fn score_health_from_content(content: &str) -> crate::agent::fix_recipes::HealthScore {
2154 crate::agent::fix_recipes::score_health(&[("report", content)])
2155}
2156
2157pub fn report_has_issues_in_content(content: &str) -> bool {
2158 for line in content.lines() {
2159 if line.contains("Health Score:") {
2160 if let Some(pos) = line.find("Score:") {
2161 let after = line[pos + 6..]
2162 .trim_start()
2163 .trim_start_matches('*')
2164 .trim_start();
2165 return !after.starts_with('A');
2166 }
2167 }
2168 }
2169 false
2170}
2171
2172pub fn fix_issue_categories() -> &'static [(&'static str, &'static str)] {
2173 &[
2174 (
2175 "Performance",
2176 "slow, lag, freeze, hang, high cpu, high ram, unresponsive",
2177 ),
2178 (
2179 "Network",
2180 "internet, wifi, offline, no connection, can't browse",
2181 ),
2182 ("DNS", "dns, name resolution, can't resolve"),
2183 ("VPN", "vpn, tunnel, remote access"),
2184 (
2185 "Disk Space",
2186 "disk full, out of space, low disk, drive full",
2187 ),
2188 (
2189 "Disk Health",
2190 "disk fail, smart error, bad sector, drive health",
2191 ),
2192 (
2193 "Slow Boot",
2194 "slow boot, startup slow, takes forever to boot",
2195 ),
2196 (
2197 "Crash / BSOD",
2198 "crash, bsod, blue screen, stop error, kernel panic",
2199 ),
2200 (
2201 "App Crashes",
2202 "app crash, not responding, application error",
2203 ),
2204 (
2205 "Windows Update",
2206 "update, windows update, patch, stuck on update",
2207 ),
2208 (
2209 "Virus / Malware",
2210 "virus, malware, hacked, threat, infected, ransomware",
2211 ),
2212 ("Firewall", "firewall, blocked port, blocked connection"),
2213 ("Printer", "printer, printing, print queue, can't print"),
2214 ("Audio", "sound, audio, no sound, speaker, mic, microphone"),
2215 ("Bluetooth", "bluetooth, headphones, wireless headset"),
2216 ("Camera", "camera, webcam, video call"),
2217 ("Teams", "teams, microsoft teams"),
2218 (
2219 "Outlook / Email",
2220 "outlook, email not working, calendar not",
2221 ),
2222 ("Browser", "browser, chrome, edge, firefox, slow browser"),
2223 (
2224 "Sign-In / PIN",
2225 "sign in, can't log in, pin not working, fingerprint, locked out",
2226 ),
2227 (
2228 "Remote Desktop",
2229 "rdp, remote desktop, can't connect remotely",
2230 ),
2231 (
2232 "Driver / Device",
2233 "device not recognized, driver not, usb not working, yellow bang",
2234 ),
2235 ("Clock / Time", "time wrong, clock wrong, time sync"),
2236 ("OneDrive", "onedrive, file sync, not syncing"),
2237 ("WMI", "wmi error, powershell wmi"),
2238 (
2239 "Display / Monitor",
2240 "monitor, display, screen resolution, second monitor, refresh rate, scaling",
2241 ),
2242 (
2243 "Keyboard / Mouse",
2244 "keyboard not working, mouse not working, touchpad, trackpad",
2245 ),
2246 (
2247 "Sleep / Hibernate",
2248 "hibernate, won't sleep, won't wake, sleep issue, stuck after sleep",
2249 ),
2250 (
2251 "Microsoft Store / Apps",
2252 "microsoft store, store app, uwp, app won't install, winget",
2253 ),
2254 (
2255 "Network Share",
2256 "network share, mapped drive, unc path, smb share, can't access share",
2257 ),
2258 (
2259 "High Network Usage",
2260 "bandwidth, high network usage, slow internet, what's using network",
2261 ),
2262 (
2263 "USB Device",
2264 "usb not recognized, usb not working, device not recognized",
2265 ),
2266 (
2267 "GPU Driver Crash",
2268 "gpu driver crash, tdr failure, nvlddmkm, black screen after driver, display driver stopped",
2269 ),
2270 (
2271 "Windows Update Stuck",
2272 "update stuck, update error 0x, update won't install, cumulative update failed",
2273 ),
2274 (
2275 "Access Denied",
2276 "access denied, can't open file, permission denied, you don't have permission",
2277 ),
2278 (
2279 "Wi-Fi Dropping",
2280 "wifi drops, wifi disconnects, internet cuts out, wifi intermittent, wifi unstable",
2281 ),
2282 (
2283 "Defender High CPU",
2284 "MsMpEng high CPU, antimalware service executable, Defender slow, Defender scan CPU",
2285 ),
2286 (
2287 "Monitor Not Detected",
2288 "second monitor not showing, HDMI not working, external monitor, no signal, DisplayPort",
2289 ),
2290 (
2291 "Explorer / Desktop Crashed",
2292 "desktop crashed, taskbar disappeared, start menu not working, File Explorer crash",
2293 ),
2294 (
2295 "Overheating / Fan",
2296 "pc overheating, fan running loud, fans spinning, thermal throttling, cpu too hot",
2297 ),
2298 (
2299 "RAM / Memory",
2300 "RAM almost full, out of memory, low memory, memory leak, running out of ram",
2301 ),
2302 (
2303 "Windows Activation",
2304 "not activated, windows license expired, activate windows, product key",
2305 ),
2306 (
2307 "BitLocker",
2308 "bitlocker asking for recovery key, drive encryption, bitlocker locked",
2309 ),
2310 (
2311 "Domain / Group Policy",
2312 "can't join domain, group policy not applying, domain controller unreachable",
2313 ),
2314 (
2315 "Hyper-V / VM",
2316 "hyper-v vm won't start, virtual machine error, vm network not working",
2317 ),
2318 (
2319 "WSL",
2320 "wsl not working, wsl2 error, linux subsystem broken",
2321 ),
2322 (
2323 "Docker",
2324 "docker container won't start, docker daemon not running, docker compose error",
2325 ),
2326 (
2327 "Random Restart",
2328 "computer restarts randomly, keeps restarting, random reboot",
2329 ),
2330 (
2331 "Disk Filling Up",
2332 "ssd getting full fast, disk filling up, recycle bin won't empty, hiberfil.sys too big",
2333 ),
2334 (
2335 "DHCP / IP Address",
2336 "no ip address, dhcp not working, ip address conflict, apipa, 169.254",
2337 ),
2338 (
2339 "Certificate / SSL",
2340 "certificate expired, ssl error, cert not trusted, tls error",
2341 ),
2342 (
2343 "TPM / Secure Boot",
2344 "tpm not detected, secure boot disabled, bitlocker needs tpm",
2345 ),
2346 (
2347 "SMB / NTLM Security",
2348 "smb1 enabled, ntlmv1 in use, lan manager authentication",
2349 ),
2350 (
2351 "Windows Search",
2352 "windows search eating disk, search indexing not working",
2353 ),
2354 (
2355 "WMI",
2356 "wmi not working, wmi corrupt, winmgmt error",
2357 ),
2358 ]
2359}
2360
2361pub async fn generate_report_markdown() -> String {
2362 let timestamp = now_timestamp_string();
2363 let mut hostname = hostname_from_env();
2364 let version = env!("CARGO_PKG_VERSION");
2365 let mut sections: Vec<(&str, String)> = Vec::with_capacity(REPORT_TOPICS.len());
2366
2367 let total = REPORT_TOPICS.len();
2368 for (i, (topic, label)) in REPORT_TOPICS.iter().enumerate() {
2369 eprintln!(" [{}/{}] {}...", i + 1, total, label);
2370 let args = json!({"topic": topic});
2371 let output = match crate::tools::host_inspect::inspect_host(&args).await {
2372 Ok(s) => {
2373 if *topic == "hardware" {
2374 for line in s.lines() {
2375 let ll = line.to_ascii_lowercase();
2376 if ll.contains("hostname") || ll.contains("computer name") {
2377 if let Some(val) = line.split_once(':').map(|x| x.1) {
2378 let h = val.trim().to_string();
2379 if !h.is_empty() {
2380 hostname = h;
2381 }
2382 }
2383 }
2384 }
2385 }
2386 s
2387 }
2388 Err(e) => format!("Error: {}", e),
2389 };
2390 sections.push((label, output));
2391 }
2392
2393 let section_refs: Vec<(&str, &str)> = sections.iter().map(|(l, o)| (*l, o.as_str())).collect();
2394 let score = crate::agent::fix_recipes::score_health(§ion_refs);
2395 let action_plan = crate::agent::fix_recipes::format_action_plan(§ion_refs);
2396
2397 let mut md = String::with_capacity(action_plan.len() + sections.len() * 512 + 256);
2398 md.push_str("# Hematite Diagnostic Report\n\n");
2399 let _ = writeln!(md, "**Generated:** {} ", timestamp);
2400 let _ = writeln!(md, "**Host:** {} ", hostname);
2401 let _ = writeln!(md, "**Hematite:** v{} ", version);
2402 let _ = write!(
2403 md,
2404 "**Health Score:** {} — {} \n\n",
2405 score.grade, score.label
2406 );
2407 let _ = write!(md, "> {}\n\n", score.summary_line());
2408 md.push_str("---\n\n");
2409
2410 md.push_str("## Action Plan\n\n");
2411 md.push_str(&action_plan);
2412 md.push_str("---\n\n");
2413
2414 for (label, output) in §ions {
2415 let _ = write!(md, "## {}\n\n", label);
2416 md.push_str("```\n");
2417 md.push_str(output.trim_end());
2418 md.push_str("\n```\n\n");
2419 }
2420
2421 md
2422}
2423
2424struct DiagnosisData {
2425 timestamp: String,
2426 hostname: String,
2427 health_output: String,
2428 follow_up_outputs: Vec<(&'static str, String)>,
2429}
2430
2431async fn run_diagnosis_phases() -> DiagnosisData {
2432 let timestamp = now_timestamp_string();
2433 let hostname = hostname_from_env();
2434
2435 eprintln!(" → System Health (scanning for issues)...");
2436 let health_args = json!({"topic": "health_report"});
2437 let health_output = match crate::tools::host_inspect::inspect_host(&health_args).await {
2438 Ok(s) => s,
2439 Err(e) => format!("Error running health_report: {}", e),
2440 };
2441
2442 let follow_up_topics = crate::agent::diagnose::triage_follow_up_topics(&health_output);
2443
2444 if follow_up_topics.is_empty() {
2445 eprintln!(" → No follow-up checks needed.");
2446 } else {
2447 eprintln!(
2448 " → {} area(s) flagged — running targeted checks...",
2449 follow_up_topics.len()
2450 );
2451 }
2452
2453 let mut follow_up_outputs: Vec<(&'static str, String)> =
2454 Vec::with_capacity(follow_up_topics.len());
2455 for (i, topic) in follow_up_topics.iter().enumerate() {
2456 eprintln!(" [{}/{}] {}...", i + 1, follow_up_topics.len(), topic);
2457 let args = json!({"topic": topic});
2458 let output = match crate::tools::host_inspect::inspect_host(&args).await {
2459 Ok(s) => s,
2460 Err(e) => format!("Error: {}", e),
2461 };
2462 follow_up_outputs.push((*topic, output));
2463 }
2464
2465 DiagnosisData {
2466 timestamp,
2467 hostname,
2468 health_output,
2469 follow_up_outputs,
2470 }
2471}
2472
2473fn correlation_md_block(combined_raw: &str) -> String {
2475 let correlations = crate::agent::correlation::correlate_findings(combined_raw);
2476 if correlations.is_empty() {
2477 return String::new();
2478 }
2479 let mut md = String::from("## Root Cause Correlation\n\n");
2480 md.push_str("> Independent findings across multiple topics point to a shared root cause.\n\n");
2481 for (i, rc) in correlations.iter().enumerate() {
2482 let _ = write!(
2483 md,
2484 "### [{}] [{}] {}\n\n{}\n\n",
2485 i + 1,
2486 rc.confidence,
2487 rc.summary,
2488 rc.detail
2489 );
2490 if !rc.unified_steps.is_empty() {
2491 md.push_str("**Consolidated fix steps:**\n\n");
2492 for (n, step) in rc.unified_steps.iter().enumerate() {
2493 let _ = writeln!(md, "{}. {}", n + 1, step);
2494 }
2495 md.push('\n');
2496 }
2497 }
2498 md.push_str("---\n\n");
2499 md
2500}
2501
2502fn correlation_html_block(combined_raw: &str) -> String {
2505 use crate::agent::html_template::he;
2506 let correlations = crate::agent::correlation::correlate_findings(combined_raw);
2507 if correlations.is_empty() {
2508 return String::new();
2509 }
2510 let mut html = String::from(
2511 "<section><h2>Root Cause Correlation</h2>\
2512 <p style=\"color:#a3a3a3;font-size:.85rem;margin-bottom:1rem\">\
2513 Independent findings across multiple topics point to a shared root cause.</p>",
2514 );
2515 for (i, rc) in correlations.iter().enumerate() {
2516 let (sev_class, badge_class) = if rc.confidence == "HIGH" {
2517 ("sev-action", "b-action")
2518 } else {
2519 ("sev-investigate", "b-investigate")
2520 };
2521 let _ = write!(
2522 html,
2523 "<div class=\"recipe {sev}\"><h3><span class=\"badge {badge}\">{conf}</span> [{n}] {summary}</h3>\
2524 <p style=\"color:#d4d4d4;font-size:.85rem;margin-bottom:.75rem\">{detail}</p>",
2525 sev = sev_class,
2526 badge = badge_class,
2527 conf = rc.confidence,
2528 n = i + 1,
2529 summary = he(rc.summary),
2530 detail = he(rc.detail),
2531 );
2532 if !rc.unified_steps.is_empty() {
2533 html.push_str("<ol>");
2534 for step in rc.unified_steps {
2535 let _ = write!(html, "<li>{}</li>", he(step));
2536 }
2537 html.push_str("</ol>");
2538 }
2539 html.push_str("</div>");
2540 }
2541 html.push_str("</section>");
2542 html
2543}
2544
2545pub async fn generate_diagnosis_report() -> String {
2548 let version = env!("CARGO_PKG_VERSION");
2549 let data = run_diagnosis_phases().await;
2550
2551 let mut section_refs: Vec<(&str, &str)> = vec![("health_report", data.health_output.as_str())];
2552 for (topic, output) in &data.follow_up_outputs {
2553 section_refs.push((*topic, output.as_str()));
2554 }
2555 let score = crate::agent::fix_recipes::score_health(§ion_refs);
2556 let action_plan = crate::agent::fix_recipes::format_action_plan(§ion_refs);
2557
2558 let combined_raw: String = std::iter::once(data.health_output.as_str())
2559 .chain(data.follow_up_outputs.iter().map(|(_, o)| o.as_str()))
2560 .collect::<Vec<_>>()
2561 .join("\n");
2562 let correlation_block = correlation_md_block(&combined_raw);
2563
2564 let mut md =
2565 String::with_capacity(action_plan.len() + data.follow_up_outputs.len() * 512 + 256);
2566 md.push_str("# Hematite Staged Diagnosis Report\n\n");
2567 let _ = writeln!(md, "**Generated:** {} ", data.timestamp);
2568 let _ = writeln!(md, "**Host:** {} ", data.hostname);
2569 let _ = writeln!(md, "**Hematite:** v{} ", version);
2570 let _ = write!(
2571 md,
2572 "**Health Score:** {} — {} \n\n",
2573 score.grade, score.label
2574 );
2575 let _ = write!(md, "> {}\n\n", score.summary_line());
2576 md.push_str("---\n\n");
2577 md.push_str("## Action Plan\n\n");
2578 md.push_str(&action_plan);
2579 md.push_str("---\n\n");
2580 md.push_str(&correlation_block);
2581 md.push_str("## System Health\n\n```\n");
2582 md.push_str(data.health_output.trim_end());
2583 md.push_str("\n```\n\n");
2584
2585 if !data.follow_up_outputs.is_empty() {
2586 md.push_str("## Targeted Investigation\n\n");
2587 for (topic, output) in &data.follow_up_outputs {
2588 let _ = write!(md, "### {}\n\n```\n", topic);
2589 md.push_str(output.trim_end());
2590 md.push_str("\n```\n\n");
2591 }
2592 }
2593
2594 md
2595}
2596
2597pub async fn generate_diagnosis_report_html() -> String {
2599 let version = env!("CARGO_PKG_VERSION");
2600 let data = run_diagnosis_phases().await;
2601
2602 let mut section_refs: Vec<(&str, &str)> = vec![("health_report", data.health_output.as_str())];
2603 for (topic, output) in &data.follow_up_outputs {
2604 section_refs.push((*topic, output.as_str()));
2605 }
2606 let score = crate::agent::fix_recipes::score_health(§ion_refs);
2607 let action_plan_html = crate::agent::fix_recipes::format_action_plan_html(§ion_refs);
2608
2609 let combined_raw: String = std::iter::once(data.health_output.as_str())
2610 .chain(data.follow_up_outputs.iter().map(|(_, o)| o.as_str()))
2611 .collect::<Vec<_>>()
2612 .join("\n");
2613 let corr_html = correlation_html_block(&combined_raw);
2614
2615 let mut sections: Vec<(&str, String)> = vec![("System Health", data.health_output.clone())];
2616 for (topic, output) in &data.follow_up_outputs {
2617 sections.push((*topic, output.clone()));
2618 }
2619
2620 build_html_document(
2621 "Hematite Staged Diagnosis",
2622 &data.timestamp,
2623 &data.hostname,
2624 version,
2625 &score,
2626 &action_plan_html,
2627 if corr_html.is_empty() {
2628 None
2629 } else {
2630 Some(&corr_html)
2631 },
2632 §ions,
2633 )
2634}
2635
2636pub async fn generate_report_json() -> String {
2637 let timestamp = now_timestamp_string();
2638 let hostname = hostname_from_env();
2639 let version = env!("CARGO_PKG_VERSION");
2640 let mut obj = serde_json::Map::new();
2641 obj.insert("generated".into(), json!(timestamp));
2642 obj.insert("host".into(), json!(hostname));
2643 obj.insert("hematite_version".into(), json!(version));
2644
2645 let total = REPORT_TOPICS.len();
2646 for (i, (topic, label)) in REPORT_TOPICS.iter().enumerate() {
2647 eprintln!(" [{}/{}] {}...", i + 1, total, label);
2648 let args = json!({"topic": topic});
2649 let value = match crate::tools::host_inspect::inspect_host(&args).await {
2650 Ok(output) => json!({"label": label, "output": output}),
2651 Err(e) => json!({"label": label, "error": e}),
2652 };
2653 obj.insert(topic.to_string(), value);
2654 }
2655
2656 serde_json::to_string_pretty(&serde_json::Value::Object(obj))
2657 .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
2658}
2659
2660pub async fn save_report_markdown() -> (String, PathBuf) {
2663 let md = generate_report_markdown().await;
2664 let path = report_path("md");
2665 ensure_parent(&path);
2666 let _ = std::fs::write(&path, &md);
2667 (md, path)
2668}
2669
2670pub async fn save_report_json() -> (String, PathBuf) {
2672 let json = generate_report_json().await;
2673 let path = report_path("json");
2674 ensure_parent(&path);
2675 let _ = std::fs::write(&path, &json);
2676 (json, path)
2677}
2678
2679pub async fn generate_report_html() -> String {
2681 let timestamp = now_timestamp_string();
2682 let mut hostname = hostname_from_env();
2683 let version = env!("CARGO_PKG_VERSION");
2684 let mut sections: Vec<(&str, String)> = Vec::with_capacity(REPORT_TOPICS.len());
2685
2686 let total = REPORT_TOPICS.len();
2687 for (i, (topic, label)) in REPORT_TOPICS.iter().enumerate() {
2688 eprintln!(" [{}/{}] {}...", i + 1, total, label);
2689 let args = json!({"topic": topic});
2690 let output = match crate::tools::host_inspect::inspect_host(&args).await {
2691 Ok(s) => {
2692 if *topic == "hardware" {
2693 for line in s.lines() {
2694 let ll = line.to_ascii_lowercase();
2695 if ll.contains("hostname") || ll.contains("computer name") {
2696 if let Some(val) = line.split_once(':').map(|x| x.1) {
2697 let h = val.trim().to_string();
2698 if !h.is_empty() {
2699 hostname = h;
2700 }
2701 }
2702 }
2703 }
2704 }
2705 s
2706 }
2707 Err(e) => format!("Error: {}", e),
2708 };
2709 sections.push((label, output));
2710 }
2711
2712 let section_refs: Vec<(&str, &str)> = sections.iter().map(|(l, o)| (*l, o.as_str())).collect();
2713 let score = crate::agent::fix_recipes::score_health(§ion_refs);
2714 let action_plan_html = crate::agent::fix_recipes::format_action_plan_html(§ion_refs);
2715
2716 build_html_document(
2717 "Hematite Diagnostic Report",
2718 ×tamp,
2719 &hostname,
2720 version,
2721 &score,
2722 &action_plan_html,
2723 None,
2724 §ions,
2725 )
2726}
2727
2728pub async fn save_report_html() -> (String, PathBuf) {
2729 let html = generate_report_html().await;
2730 let path = report_path("html");
2731 ensure_parent(&path);
2732 let _ = std::fs::write(&path, &html);
2733 (html, path)
2734}
2735
2736pub async fn save_diagnosis_report() -> (String, PathBuf) {
2737 let md = generate_diagnosis_report().await;
2738 let path = crate::tools::file_ops::hematite_dir()
2739 .join("reports")
2740 .join(format!("diagnosis-{}.md", now_file_timestamp()));
2741 ensure_parent(&path);
2742 let _ = std::fs::write(&path, &md);
2743 (md, path)
2744}
2745
2746pub async fn save_diagnosis_report_html() -> (String, PathBuf) {
2747 let html = generate_diagnosis_report_html().await;
2748 let path = crate::tools::file_ops::hematite_dir()
2749 .join("reports")
2750 .join(format!("diagnosis-{}.html", now_file_timestamp()));
2751 ensure_parent(&path);
2752 let _ = std::fs::write(&path, &html);
2753 (html, path)
2754}
2755
2756pub async fn generate_diagnosis_report_json() -> String {
2757 let version = env!("CARGO_PKG_VERSION");
2758 let data = run_diagnosis_phases().await;
2759 let mut section_refs: Vec<(&str, &str)> = vec![("health_report", data.health_output.as_str())];
2760 for (topic, output) in &data.follow_up_outputs {
2761 section_refs.push((*topic, output.as_str()));
2762 }
2763 let score = crate::agent::fix_recipes::score_health(§ion_refs);
2764
2765 let mut seen_titles = std::collections::HashSet::new();
2767 let mut action_items: Vec<&str> = Vec::new();
2768 let mut investigate_items: Vec<&str> = Vec::new();
2769 for (_label, output) in §ion_refs {
2770 for recipe in crate::agent::fix_recipes::match_recipes(output) {
2771 if seen_titles.insert(recipe.title) {
2772 match recipe.severity {
2773 "ACTION" => action_items.push(recipe.title),
2774 "INVESTIGATE" => investigate_items.push(recipe.title),
2775 _ => {}
2776 }
2777 }
2778 }
2779 }
2780
2781 let mut sections_obj = serde_json::Map::new();
2782 sections_obj.insert("health_report".into(), json!(data.health_output));
2783 for (topic, output) in &data.follow_up_outputs {
2784 sections_obj.insert(topic.to_string(), json!(output));
2785 }
2786
2787 let obj = json!({
2788 "generated": data.timestamp,
2789 "host": data.hostname,
2790 "hematite_version": version,
2791 "grade": score.grade.to_string(),
2792 "label": score.label,
2793 "action_count": score.action_count,
2794 "investigate_count": score.investigate_count,
2795 "monitor_count": score.monitor_count,
2796 "action_items": action_items,
2797 "investigate_items": investigate_items,
2798 "follow_up_topics": data.follow_up_outputs.iter().map(|(t, _)| *t).collect::<Vec<_>>(),
2799 "sections": serde_json::Value::Object(sections_obj),
2800 });
2801 serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
2802}
2803
2804pub async fn save_diagnosis_report_json() -> (String, PathBuf) {
2805 let json = generate_diagnosis_report_json().await;
2806 let path = crate::tools::file_ops::hematite_dir()
2807 .join("reports")
2808 .join(format!("diagnosis-{}.json", now_file_timestamp()));
2809 ensure_parent(&path);
2810 let _ = std::fs::write(&path, &json);
2811 (json, path)
2812}
2813
2814fn build_html_document(
2815 title: &str,
2816 timestamp: &str,
2817 hostname: &str,
2818 version: &str,
2819 score: &crate::agent::fix_recipes::HealthScore,
2820 action_plan_html: &str,
2821 correlation_html: Option<&str>,
2822 sections: &[(&str, String)],
2823) -> String {
2824 use crate::agent::html_template::{build_html_shell, he, COPY_BUTTON_HTML};
2825
2826 let mut sections_html =
2827 String::with_capacity(sections.iter().map(|(_, o)| o.len() + 64).sum::<usize>());
2828 for (label, output) in sections {
2829 let _ = writeln!(
2830 sections_html,
2831 "<details><summary>{}</summary><pre>{}</pre></details>",
2832 he(label),
2833 he(output.trim_end())
2834 );
2835 }
2836
2837 let corr_section = correlation_html.unwrap_or("");
2838
2839 let content = format!(
2840 r#"<header>
2841<h1>{title}</h1>
2842<div class="meta">
2843 <span>Generated: {timestamp}</span>
2844 <span>Host: {hostname}</span>
2845 <span>Hematite v{version}</span>
2846</div>
2847<div class="score-row">
2848 <div class="grade g{grade}">{grade}</div>
2849 <div class="score-info">
2850 <h2>Health Score: {grade} — {label}</h2>
2851 <p>{summary}</p>
2852 </div>
2853</div>
2854<p class="grade-intro">{intro}</p>
2855{copy_btn}
2856</header>
2857<section>
2858<h2>Action Plan</h2>
2859{action_plan_html}
2860</section>
2861{corr_section}
2862<section>
2863<h2>Diagnostic Data</h2>
2864{sections_html}
2865</section>"#,
2866 title = he(title),
2867 hostname = he(hostname),
2868 timestamp = he(timestamp),
2869 version = he(version),
2870 grade = score.grade,
2871 label = he(score.label),
2872 summary = he(&score.summary_line()),
2873 intro = he(score.grade_intro()),
2874 copy_btn = COPY_BUTTON_HTML,
2875 action_plan_html = action_plan_html,
2876 corr_section = corr_section,
2877 sections_html = sections_html,
2878 );
2879
2880 let page_title = format!("{} — {}", he(title), he(hostname));
2881 build_html_shell(&page_title, version, &content)
2882}
2883
2884struct TriageData {
2887 timestamp: String,
2888 hostname: String,
2889 sections: Vec<(&'static str, String)>,
2890}
2891
2892async fn run_triage_phases(preset: &str) -> TriageData {
2893 let topics = triage_topics_for_preset(preset);
2894 let total = topics.len();
2895 let timestamp = now_timestamp_string();
2896 let mut hostname = hostname_from_env();
2897 let mut sections: Vec<(&'static str, String)> = Vec::with_capacity(total);
2898
2899 for (i, &(topic, label)) in topics.iter().enumerate() {
2900 eprintln!(" [{}/{}] {}...", i + 1, total, label);
2901 let args = serde_json::json!({"topic": topic});
2902 let output = match crate::tools::host_inspect::inspect_host(&args).await {
2903 Ok(s) => {
2904 if topic == "health_report" {
2905 for line in s.lines() {
2906 let ll = line.to_ascii_lowercase();
2907 if ll.contains("hostname") || ll.contains("computer name") {
2908 if let Some(val) = line.split_once(':').map(|x| x.1) {
2909 let h = val.trim().to_string();
2910 if !h.is_empty() {
2911 hostname = h;
2912 }
2913 }
2914 }
2915 }
2916 }
2917 s
2918 }
2919 Err(e) => format!("Error: {}", e),
2920 };
2921 sections.push((label, output));
2922 }
2923
2924 TriageData {
2925 timestamp,
2926 hostname,
2927 sections,
2928 }
2929}
2930
2931pub async fn generate_triage_report_markdown(preset: &str) -> String {
2932 let title = triage_preset_title(preset);
2933 let data = run_triage_phases(preset).await;
2934 let version = env!("CARGO_PKG_VERSION");
2935
2936 let section_refs: Vec<(&str, &str)> = data
2937 .sections
2938 .iter()
2939 .map(|(l, o)| (*l, o.as_str()))
2940 .collect();
2941 let score = crate::agent::fix_recipes::score_health(§ion_refs);
2942 let action_plan = crate::agent::fix_recipes::format_action_plan(§ion_refs);
2943
2944 let combined_raw: String = data
2945 .sections
2946 .iter()
2947 .map(|(_, o)| o.as_str())
2948 .collect::<Vec<_>>()
2949 .join("\n");
2950 let correlation_block = correlation_md_block(&combined_raw);
2951
2952 let mut md = String::with_capacity(action_plan.len() + data.sections.len() * 512 + 256);
2953 let _ = write!(md, "# {}\n\n", title);
2954 let _ = writeln!(md, "**Generated:** {} ", data.timestamp);
2955 let _ = writeln!(md, "**Host:** {} ", data.hostname);
2956 let _ = writeln!(md, "**Hematite:** v{} ", version);
2957 let _ = write!(
2958 md,
2959 "**Health Score:** {} — {} \n\n",
2960 score.grade, score.label
2961 );
2962 let _ = write!(md, "> {}\n\n", score.summary_line());
2963 md.push_str("---\n\n## Action Plan\n\n");
2964 md.push_str(&action_plan);
2965 md.push_str("---\n\n");
2966 md.push_str(&correlation_block);
2967 for (label, output) in &data.sections {
2968 let _ = write!(md, "## {}\n\n```\n", label);
2969 md.push_str(output.trim_end());
2970 md.push_str("\n```\n\n");
2971 }
2972 md
2973}
2974
2975pub async fn generate_triage_report_html(preset: &str) -> String {
2976 let title = triage_preset_title(preset);
2977 let data = run_triage_phases(preset).await;
2978 let version = env!("CARGO_PKG_VERSION");
2979
2980 let section_refs: Vec<(&str, &str)> = data
2981 .sections
2982 .iter()
2983 .map(|(l, o)| (*l, o.as_str()))
2984 .collect();
2985 let score = crate::agent::fix_recipes::score_health(§ion_refs);
2986 let action_plan_html = crate::agent::fix_recipes::format_action_plan_html(§ion_refs);
2987
2988 let combined_raw: String = data
2989 .sections
2990 .iter()
2991 .map(|(_, o)| o.as_str())
2992 .collect::<Vec<_>>()
2993 .join("\n");
2994 let corr_html = correlation_html_block(&combined_raw);
2995
2996 build_html_document(
2997 title,
2998 &data.timestamp,
2999 &data.hostname,
3000 version,
3001 &score,
3002 &action_plan_html,
3003 if corr_html.is_empty() {
3004 None
3005 } else {
3006 Some(&corr_html)
3007 },
3008 &data.sections,
3009 )
3010}
3011
3012pub async fn save_triage_report(preset: &str) -> (String, PathBuf) {
3013 let md = generate_triage_report_markdown(preset).await;
3014 let path = crate::tools::file_ops::hematite_dir()
3015 .join("reports")
3016 .join(format!("triage-{}.md", now_file_timestamp()));
3017 ensure_parent(&path);
3018 let _ = std::fs::write(&path, &md);
3019 (md, path)
3020}
3021
3022pub async fn save_triage_report_html(preset: &str) -> (String, PathBuf) {
3023 let html = generate_triage_report_html(preset).await;
3024 let path = crate::tools::file_ops::hematite_dir()
3025 .join("reports")
3026 .join(format!("triage-{}.html", now_file_timestamp()));
3027 ensure_parent(&path);
3028 let _ = std::fs::write(&path, &html);
3029 (html, path)
3030}
3031
3032pub async fn generate_triage_report_json(preset: &str) -> String {
3033 let data = run_triage_phases(preset).await;
3034 let version = env!("CARGO_PKG_VERSION");
3035 let section_refs: Vec<(&str, &str)> = data
3036 .sections
3037 .iter()
3038 .map(|(l, o)| (*l, o.as_str()))
3039 .collect();
3040 let score = crate::agent::fix_recipes::score_health(§ion_refs);
3041
3042 let mut seen_titles = std::collections::HashSet::new();
3043 let mut action_items: Vec<&str> = Vec::new();
3044 let mut investigate_items: Vec<&str> = Vec::new();
3045 for (_label, output) in §ion_refs {
3046 for recipe in crate::agent::fix_recipes::match_recipes(output) {
3047 if seen_titles.insert(recipe.title) {
3048 match recipe.severity {
3049 "ACTION" => action_items.push(recipe.title),
3050 "INVESTIGATE" => investigate_items.push(recipe.title),
3051 _ => {}
3052 }
3053 }
3054 }
3055 }
3056
3057 let mut sections_obj = serde_json::Map::new();
3058 for (label, output) in &data.sections {
3059 sections_obj.insert(label.to_string(), json!(output));
3060 }
3061
3062 let obj = json!({
3063 "generated": data.timestamp,
3064 "host": data.hostname,
3065 "hematite_version": version,
3066 "preset": preset,
3067 "grade": score.grade.to_string(),
3068 "label": score.label,
3069 "action_count": score.action_count,
3070 "investigate_count": score.investigate_count,
3071 "monitor_count": score.monitor_count,
3072 "action_items": action_items,
3073 "investigate_items": investigate_items,
3074 "sections": serde_json::Value::Object(sections_obj),
3075 });
3076 serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
3077}
3078
3079pub async fn save_triage_report_json(preset: &str) -> (String, PathBuf) {
3080 let json = generate_triage_report_json(preset).await;
3081 let path = crate::tools::file_ops::hematite_dir()
3082 .join("reports")
3083 .join(format!("triage-{}.json", now_file_timestamp()));
3084 ensure_parent(&path);
3085 let _ = std::fs::write(&path, &json);
3086 (json, path)
3087}
3088
3089struct FixPlanData {
3092 timestamp: String,
3093 hostname: String,
3094 sections: Vec<(&'static str, String)>,
3095}
3096
3097async fn run_fix_plan_phases(issue: &str) -> FixPlanData {
3098 let initial_topics = topics_for_issue(issue);
3099 let total = initial_topics.len();
3100 let timestamp = now_timestamp_string();
3101 let mut hostname = hostname_from_env();
3102 let mut sections: Vec<(&'static str, String)> = Vec::with_capacity(total);
3103
3104 eprintln!("hematite --fix: \"{}\" ({} check(s))", issue, total);
3105 for (i, &(topic, label)) in initial_topics.iter().enumerate() {
3106 eprintln!(" [{}/{}] {}...", i + 1, total, label);
3107 let args = serde_json::json!({"topic": topic});
3108 let output = match crate::tools::host_inspect::inspect_host(&args).await {
3109 Ok(s) => {
3110 if topic == "health_report" {
3111 for line in s.lines() {
3112 let ll = line.to_ascii_lowercase();
3113 if ll.contains("hostname") || ll.contains("computer name") {
3114 if let Some(val) = line.split_once(':').map(|x| x.1) {
3115 let h = val.trim().to_string();
3116 if !h.is_empty() {
3117 hostname = h;
3118 }
3119 }
3120 }
3121 }
3122 }
3123 s
3124 }
3125 Err(e) => format!("Error: {}", e),
3126 };
3127 sections.push((label, output));
3128 }
3129
3130 let combined: String = {
3131 let total = sections.iter().map(|(_, o)| o.len()).sum::<usize>() + sections.len();
3132 let mut s = String::with_capacity(total);
3133 for (i, (_, o)) in sections.iter().enumerate() {
3134 if i > 0 {
3135 s.push('\n');
3136 }
3137 s.push_str(o);
3138 }
3139 s
3140 };
3141 let ran: Vec<&str> = initial_topics.iter().map(|&(t, _)| t).collect();
3142 let follow_ups = crate::agent::diagnose::fix_follow_up_topics(&combined, &ran);
3143
3144 if !follow_ups.is_empty() {
3145 eprintln!(
3146 " → {} follow-up check(s) triggered by findings...",
3147 follow_ups.len()
3148 );
3149 }
3150
3151 for (i, &(topic, label)) in follow_ups.iter().enumerate() {
3152 eprintln!(" + [{}/{}] {}...", i + 1, follow_ups.len(), label);
3153 let args = serde_json::json!({"topic": topic});
3154 let output = match crate::tools::host_inspect::inspect_host(&args).await {
3155 Ok(s) => s,
3156 Err(e) => format!("Error: {}", e),
3157 };
3158 sections.push((label, output));
3159 }
3160
3161 FixPlanData {
3162 timestamp,
3163 hostname,
3164 sections,
3165 }
3166}
3167
3168pub async fn generate_fix_plan_markdown(issue: &str) -> String {
3169 let data = run_fix_plan_phases(issue).await;
3170 let version = env!("CARGO_PKG_VERSION");
3171
3172 let section_refs: Vec<(&str, &str)> = data
3173 .sections
3174 .iter()
3175 .map(|(l, o)| (*l, o.as_str()))
3176 .collect();
3177 let score = crate::agent::fix_recipes::score_health(§ion_refs);
3178 let action_plan = crate::agent::fix_recipes::format_action_plan(§ion_refs);
3179
3180 let mut md = String::with_capacity(action_plan.len() + data.sections.len() * 512 + 256);
3181 md.push_str("# Hematite Fix Plan\n\n");
3182 let _ = writeln!(md, "**Issue:** {} ", issue);
3183 let _ = writeln!(md, "**Generated:** {} ", data.timestamp);
3184 let _ = writeln!(md, "**Host:** {} ", data.hostname);
3185 let _ = writeln!(md, "**Hematite:** v{} ", version);
3186 let _ = write!(
3187 md,
3188 "**Health Score:** {} — {} \n\n",
3189 score.grade, score.label
3190 );
3191 let _ = write!(md, "> {}\n\n", score.summary_line());
3192 md.push_str("---\n\n## Fix Steps\n\n");
3193 md.push_str(&action_plan);
3194 md.push_str("---\n\n");
3195 let combined_raw: String = data
3196 .sections
3197 .iter()
3198 .map(|(_, o)| o.as_str())
3199 .collect::<Vec<_>>()
3200 .join("\n");
3201 let correlation_block = correlation_md_block(&combined_raw);
3202 md.push_str(&correlation_block);
3203 for (label, output) in &data.sections {
3204 let _ = write!(md, "## {}\n\n```\n", label);
3205 md.push_str(output.trim_end());
3206 md.push_str("\n```\n\n");
3207 }
3208 md
3209}
3210
3211pub async fn generate_fix_plan_html(issue: &str) -> String {
3212 let data = run_fix_plan_phases(issue).await;
3213 let version = env!("CARGO_PKG_VERSION");
3214
3215 let section_refs: Vec<(&str, &str)> = data
3216 .sections
3217 .iter()
3218 .map(|(l, o)| (*l, o.as_str()))
3219 .collect();
3220 let score = crate::agent::fix_recipes::score_health(§ion_refs);
3221 let action_plan_html = crate::agent::fix_recipes::format_action_plan_html(§ion_refs);
3222
3223 use crate::agent::html_template::{build_html_shell, he, COPY_BUTTON_HTML};
3224
3225 let mut sections_html = String::with_capacity(data.sections.len() * 512);
3226 for (label, output) in &data.sections {
3227 let _ = writeln!(
3228 sections_html,
3229 "<details><summary>{}</summary><pre>{}</pre></details>",
3230 he(label),
3231 he(output.trim_end())
3232 );
3233 }
3234
3235 let combined_raw: String = data
3236 .sections
3237 .iter()
3238 .map(|(_, o)| o.as_str())
3239 .collect::<Vec<_>>()
3240 .join("\n");
3241 let corr_html = correlation_html_block(&combined_raw);
3242 let corr_section = if corr_html.is_empty() {
3243 String::new()
3244 } else {
3245 format!("<section>\n<h2>Root Cause Correlation</h2>\n{corr_html}\n</section>")
3246 };
3247
3248 let content = format!(
3249 r#"<header>
3250<h1>Fix Plan</h1>
3251<p class="grade-intro" style="margin-bottom:.85rem">Issue: <strong>{issue}</strong></p>
3252<div class="meta">
3253 <span>Generated: {timestamp}</span>
3254 <span>Host: {hostname}</span>
3255 <span>Hematite v{version}</span>
3256</div>
3257<div class="score-row">
3258 <div class="grade g{grade}">{grade}</div>
3259 <div class="score-info">
3260 <h2>Health Score: {grade} — {label}</h2>
3261 <p>{summary}</p>
3262 </div>
3263</div>
3264{copy_btn}
3265</header>
3266<section>
3267<h2>Fix Steps</h2>
3268{action_plan_html}
3269</section>
3270{corr_section}
3271<section>
3272<h2>Diagnostic Data</h2>
3273{sections_html}
3274</section>"#,
3275 issue = he(issue),
3276 hostname = he(&data.hostname),
3277 timestamp = he(&data.timestamp),
3278 version = he(version),
3279 grade = score.grade,
3280 label = he(score.label),
3281 summary = he(&score.summary_line()),
3282 copy_btn = COPY_BUTTON_HTML,
3283 action_plan_html = action_plan_html,
3284 corr_section = corr_section,
3285 sections_html = sections_html,
3286 );
3287
3288 let page_title = format!("Fix Plan: {} — {}", he(issue), he(&data.hostname));
3289 build_html_shell(&page_title, version, &content)
3290}
3291
3292pub async fn save_fix_plan(issue: &str) -> (String, PathBuf) {
3293 let md = generate_fix_plan_markdown(issue).await;
3294 let path = crate::tools::file_ops::hematite_dir()
3295 .join("reports")
3296 .join(format!("fix-{}.md", now_file_timestamp()));
3297 ensure_parent(&path);
3298 let _ = std::fs::write(&path, &md);
3299 (md, path)
3300}
3301
3302pub async fn save_fix_plan_with_summary(issue: &str) -> (String, String, PathBuf) {
3305 let data = run_fix_plan_phases(issue).await;
3306 let version = env!("CARGO_PKG_VERSION");
3307
3308 let section_refs: Vec<(&str, &str)> = data
3309 .sections
3310 .iter()
3311 .map(|(l, o)| (*l, o.as_str()))
3312 .collect();
3313 let score = crate::agent::fix_recipes::score_health(§ion_refs);
3314 let action_plan = crate::agent::fix_recipes::format_action_plan(§ion_refs);
3315
3316 let mut md = String::with_capacity(action_plan.len() + data.sections.len() * 512 + 256);
3317 md.push_str("# Hematite Fix Plan\n\n");
3318 let _ = writeln!(md, "**Issue:** {} ", issue);
3319 let _ = writeln!(md, "**Generated:** {} ", data.timestamp);
3320 let _ = writeln!(md, "**Host:** {} ", data.hostname);
3321 let _ = writeln!(md, "**Hematite:** v{} ", version);
3322 let _ = write!(
3323 md,
3324 "**Health Score:** {} — {} \n\n",
3325 score.grade, score.label
3326 );
3327 let _ = write!(md, "> {}\n\n", score.summary_line());
3328 md.push_str("---\n\n## Fix Steps\n\n");
3329 md.push_str(&action_plan);
3330 md.push_str("---\n\n");
3331 let combined_raw: String = data
3332 .sections
3333 .iter()
3334 .map(|(_, o)| o.as_str())
3335 .collect::<Vec<_>>()
3336 .join("\n");
3337 let correlation_block = correlation_md_block(&combined_raw);
3338 md.push_str(&correlation_block);
3339 for (label, output) in &data.sections {
3340 let _ = write!(md, "## {}\n\n```\n", label);
3341 md.push_str(output.trim_end());
3342 md.push_str("\n```\n\n");
3343 }
3344
3345 let path = crate::tools::file_ops::hematite_dir()
3346 .join("reports")
3347 .join(format!("fix-{}.md", now_file_timestamp()));
3348 ensure_parent(&path);
3349 let _ = std::fs::write(&path, &md);
3350
3351 let summary = format!(
3352 "Health Score: {} — {}\n\n{}",
3353 score.grade, score.label, action_plan
3354 );
3355 (summary, md, path)
3356}
3357
3358pub async fn save_fix_plan_html(issue: &str) -> (String, PathBuf) {
3359 let html = generate_fix_plan_html(issue).await;
3360 let path = crate::tools::file_ops::hematite_dir()
3361 .join("reports")
3362 .join(format!("fix-{}.html", now_file_timestamp()));
3363 ensure_parent(&path);
3364 let _ = std::fs::write(&path, &html);
3365 (html, path)
3366}
3367
3368pub async fn save_fix_plan_json(issue: &str) -> (String, PathBuf) {
3369 let data = run_fix_plan_phases(issue).await;
3370 let version = env!("CARGO_PKG_VERSION");
3371 let section_refs: Vec<(&str, &str)> = data
3372 .sections
3373 .iter()
3374 .map(|(l, o)| (*l, o.as_str()))
3375 .collect();
3376 let score = crate::agent::fix_recipes::score_health(§ion_refs);
3377 let action_plan = crate::agent::fix_recipes::format_action_plan(§ion_refs);
3378
3379 let mut seen_titles = std::collections::HashSet::new();
3380 let mut action_items: Vec<&str> = Vec::new();
3381 let mut investigate_items: Vec<&str> = Vec::new();
3382 for (_label, output) in §ion_refs {
3383 for recipe in crate::agent::fix_recipes::match_recipes(output) {
3384 if seen_titles.insert(recipe.title) {
3385 match recipe.severity {
3386 "ACTION" => action_items.push(recipe.title),
3387 "INVESTIGATE" => investigate_items.push(recipe.title),
3388 _ => {}
3389 }
3390 }
3391 }
3392 }
3393
3394 let mut sections_obj = serde_json::Map::new();
3395 for (label, output) in &data.sections {
3396 sections_obj.insert(label.to_string(), json!(output));
3397 }
3398
3399 let obj = json!({
3400 "generated": data.timestamp,
3401 "host": data.hostname,
3402 "hematite_version": version,
3403 "issue": issue,
3404 "grade": score.grade.to_string(),
3405 "label": score.label,
3406 "action_count": score.action_count,
3407 "investigate_count": score.investigate_count,
3408 "action_items": action_items,
3409 "investigate_items": investigate_items,
3410 "action_plan": action_plan,
3411 "sections": serde_json::Value::Object(sections_obj),
3412 });
3413 let json_str =
3414 serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
3415 let path = crate::tools::file_ops::hematite_dir()
3416 .join("reports")
3417 .join(format!("fix-{}.json", now_file_timestamp()));
3418 ensure_parent(&path);
3419 let _ = std::fs::write(&path, &json_str);
3420 (json_str, path)
3421}
3422
3423pub async fn generate_inspect_output(topics_csv: &str) -> String {
3428 let topics: Vec<&str> = topics_csv
3429 .split(',')
3430 .map(str::trim)
3431 .filter(|s| !s.is_empty())
3432 .collect();
3433
3434 if topics.is_empty() {
3435 return "No topics specified. Example: hematite --inspect wifi,latency,dns_cache\n\
3436 Run `hematite --inventory` to list all 128+ available topics.\n"
3437 .to_string();
3438 }
3439
3440 let total = topics.len();
3441 if total > 1 {
3442 eprintln!("hematite --inspect: {} topic(s)", total);
3443 }
3444 let mut out = String::new();
3445 for (i, topic) in topics.iter().enumerate() {
3446 if total > 1 {
3447 eprintln!(" [{}/{}] {}...", i + 1, total, topic);
3448 }
3449 let args = json!({"topic": topic});
3450 let result = match crate::tools::host_inspect::inspect_host(&args).await {
3451 Ok(s) => s,
3452 Err(e) => format!("Error ({}): {}", topic, e),
3453 };
3454 if total > 1 {
3455 let _ = writeln!(out, "─── {} ───", topic);
3456 }
3457 out.push_str(result.trim_end());
3458 out.push('\n');
3459 if total > 1 {
3460 out.push('\n');
3461 }
3462 }
3463 out
3464}
3465
3466pub async fn generate_inspect_output_json(topics_csv: &str) -> String {
3468 let topics: Vec<&str> = topics_csv
3469 .split(',')
3470 .map(str::trim)
3471 .filter(|s| !s.is_empty())
3472 .collect();
3473
3474 if topics.is_empty() {
3475 return json!({"error": "No topics specified."}).to_string();
3476 }
3477
3478 let total = topics.len();
3479 eprintln!("hematite --inspect (json): {} topic(s)", total);
3480 let mut sections_obj = serde_json::Map::new();
3481 let mut combined = String::new();
3482 for (i, topic) in topics.iter().enumerate() {
3483 eprintln!(" [{}/{}] {}...", i + 1, total, topic);
3484 let args = json!({"topic": topic});
3485 let result = match crate::tools::host_inspect::inspect_host(&args).await {
3486 Ok(s) => s,
3487 Err(e) => format!("Error ({}): {}", topic, e),
3488 };
3489 sections_obj.insert(topic.to_string(), json!(result));
3490 combined.push_str(result.trim_end());
3491 combined.push('\n');
3492 }
3493
3494 let host = std::env::var("COMPUTERNAME")
3495 .or_else(|_| std::env::var("HOSTNAME"))
3496 .unwrap_or_else(|_| "unknown".to_string());
3497 let obj = json!({
3498 "generated": now_timestamp_string(),
3499 "host": host,
3500 "hematite_version": env!("CARGO_PKG_VERSION"),
3501 "topics": topics,
3502 "sections": serde_json::Value::Object(sections_obj),
3503 "combined_output": combined,
3504 });
3505 serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
3506}
3507
3508pub async fn run_inspect_topics(
3511 topics_csv: &str,
3512 fmt: &str,
3513 save: bool,
3514) -> (String, Option<PathBuf>) {
3515 let content = if fmt == "json" {
3516 generate_inspect_output_json(topics_csv).await
3517 } else {
3518 generate_inspect_output(topics_csv).await
3519 };
3520
3521 if !save {
3522 return (content, None);
3523 }
3524
3525 let path = crate::tools::file_ops::hematite_dir()
3526 .join("reports")
3527 .join(format!("inspect-{}.{}", now_file_timestamp(), fmt));
3528 ensure_parent(&path);
3529 let _ = std::fs::write(&path, &content);
3530 (content, Some(path))
3531}
3532
3533pub async fn generate_query_output(query: &str) -> String {
3537 use crate::agent::routing::{all_host_inspection_topics, preferred_host_inspection_topic};
3538
3539 let detected = all_host_inspection_topics(query);
3540 let topics: Vec<&str> = if !detected.is_empty() {
3541 detected
3542 } else {
3543 match preferred_host_inspection_topic(query) {
3544 Some(t) => vec![t],
3545 None => vec!["summary"],
3546 }
3547 };
3548
3549 let total = topics.len();
3550 eprintln!("hematite --query: {} topic(s) matched", total);
3551 let mut out = String::new();
3552 for (i, topic) in topics.iter().enumerate() {
3553 eprintln!(" [{}/{}] {}...", i + 1, total, topic);
3554 let args = json!({"topic": topic});
3555 let result = match crate::tools::host_inspect::inspect_host(&args).await {
3556 Ok(s) => s,
3557 Err(e) => format!("Error ({}): {}", topic, e),
3558 };
3559 if total > 1 {
3560 let _ = writeln!(out, "─── {} ───", topic);
3561 }
3562 out.push_str(result.trim_end());
3563 out.push('\n');
3564 if total > 1 {
3565 out.push('\n');
3566 }
3567 }
3568 out
3569}
3570
3571pub async fn generate_query_output_json(query: &str) -> String {
3573 use crate::agent::routing::{all_host_inspection_topics, preferred_host_inspection_topic};
3574
3575 let detected = all_host_inspection_topics(query);
3576 let topics: Vec<&str> = if !detected.is_empty() {
3577 detected
3578 } else {
3579 match preferred_host_inspection_topic(query) {
3580 Some(t) => vec![t],
3581 None => vec!["summary"],
3582 }
3583 };
3584
3585 let total = topics.len();
3586 eprintln!("hematite --query (json): {} topic(s) matched", total);
3587 let mut sections_obj = serde_json::Map::new();
3588 let mut combined = String::new();
3589 for (i, topic) in topics.iter().enumerate() {
3590 eprintln!(" [{}/{}] {}...", i + 1, total, topic);
3591 let args = json!({"topic": topic});
3592 let result = match crate::tools::host_inspect::inspect_host(&args).await {
3593 Ok(s) => s,
3594 Err(e) => format!("Error ({}): {}", topic, e),
3595 };
3596 sections_obj.insert(topic.to_string(), json!(result));
3597 combined.push_str(result.trim_end());
3598 combined.push('\n');
3599 }
3600
3601 let obj = json!({
3602 "query": query,
3603 "matched_topics": topics,
3604 "sections": serde_json::Value::Object(sections_obj),
3605 "combined_output": combined,
3606 });
3607 serde_json::to_string_pretty(&obj).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e))
3608}
3609
3610pub fn save_research_html(title: &str, body_md: &str) -> (String, PathBuf) {
3614 use crate::agent::html_template::{build_html_shell, he, markdown_to_html, COPY_BUTTON_HTML};
3615 let version = env!("CARGO_PKG_VERSION");
3616 let timestamp = now_timestamp_string();
3617 let display_title = if title.trim().is_empty() {
3618 format!("Research — {}", ×tamp[..10])
3619 } else {
3620 title.to_string()
3621 };
3622
3623 let body_html = markdown_to_html(body_md);
3624 let content = format!(
3625 r#"<header>
3626<h1>{title}</h1>
3627<div class="meta">
3628 <span>Saved: {timestamp}</span>
3629 <span>Hematite v{version}</span>
3630</div>
3631{copy_btn}
3632</header>
3633<section>
3634{body_html}
3635</section>"#,
3636 title = he(&display_title),
3637 timestamp = he(×tamp),
3638 version = he(version),
3639 copy_btn = COPY_BUTTON_HTML,
3640 body_html = body_html,
3641 );
3642
3643 let html = build_html_shell(&display_title, version, &content);
3644 let path = crate::tools::file_ops::hematite_dir()
3645 .join("reports")
3646 .join(format!("research-{}.html", now_file_timestamp()));
3647 ensure_parent(&path);
3648 let _ = std::fs::write(&path, &html);
3649 (html, path)
3650}
3651
3652fn report_path(ext: &str) -> PathBuf {
3653 crate::tools::file_ops::hematite_dir()
3654 .join("reports")
3655 .join(format!("health-{}.{}", now_file_timestamp(), ext))
3656}
3657
3658fn ensure_parent(path: &Path) {
3659 if let Some(parent) = path.parent() {
3660 let _ = std::fs::create_dir_all(parent);
3661 }
3662}
3663
3664pub fn timestamp_label() -> String {
3665 now_timestamp_string()
3666}
3667
3668fn now_timestamp_string() -> String {
3669 let now = unix_now();
3670 let (y, mo, d, h, mi, s) = epoch_to_ymd_hms(now);
3671 format!(
3672 "{:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
3673 y, mo, d, h, mi, s
3674 )
3675}
3676
3677fn now_file_timestamp() -> String {
3678 let now = unix_now();
3679 let (y, mo, d, h, mi, _s) = epoch_to_ymd_hms(now);
3680 format!("{:04}-{:02}-{:02}_{:02}-{:02}", y, mo, d, h, mi)
3681}
3682
3683fn unix_now() -> u64 {
3684 std::time::SystemTime::now()
3685 .duration_since(std::time::UNIX_EPOCH)
3686 .unwrap_or_default()
3687 .as_secs()
3688}
3689
3690fn hostname_from_env() -> String {
3691 std::env::var("COMPUTERNAME")
3692 .or_else(|_| std::env::var("HOSTNAME"))
3693 .unwrap_or_else(|_| "unknown".to_string())
3694}
3695
3696fn epoch_to_ymd_hms(epoch: u64) -> (u32, u32, u32, u32, u32, u32) {
3698 let s = (epoch % 60) as u32;
3699 let mi = ((epoch / 60) % 60) as u32;
3700 let h = ((epoch / 3600) % 24) as u32;
3701 let days = epoch / 86400;
3702
3703 let years_400 = days / 146097;
3704 let rem = days % 146097;
3705 let years_100 = rem.min(146096) / 36524;
3706 let rem = rem - years_100 * 36524;
3707 let years_4 = rem / 1461;
3708 let rem = rem % 1461;
3709 let years_1 = rem.min(1460) / 365;
3710 let rem = rem - years_1 * 365;
3711
3712 let year = (1970 + years_400 * 400 + years_100 * 100 + years_4 * 4 + years_1) as u32;
3713 let leap = u32::from(
3714 year.is_multiple_of(4) && (!year.is_multiple_of(100) || year.is_multiple_of(400)),
3715 );
3716 let month_days: [u32; 12] = [31, 28 + leap, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
3717 let mut rem = rem as u32;
3718 let mut month = 1u32;
3719 for &md in &month_days {
3720 if rem < md {
3721 break;
3722 }
3723 rem -= md;
3724 month += 1;
3725 }
3726 let day = rem + 1;
3727 (year, month, day, h, mi, s)
3728}