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