commit 3e432fbb500907394a08ff66e9c3a2b62a26fe1b Author: taqin Date: Sun Apr 12 20:42:34 2026 +0700 Initial commit: Driver Booster Pro - Go + WebView2 desktop app Windows system utility with driver scanning, Windows Update integration, and system info collection. Beautiful dark-themed UI via embedded WebView2. Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e653624 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Binaries +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary +*.test + +# Output +/driver-booster + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..97a0b74 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/mumur/driver-booster + +go 1.26.2 + +require ( + github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 // indirect + github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect + golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3e70c30 --- /dev/null +++ b/go.sum @@ -0,0 +1,7 @@ +github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 h1:ftnsTqIUH57XQEF+PnXX9++nlHCzdkuB5zbWyMMruZo= +github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808/go.mod h1:rWifBlzkgrvd7zUqlfq91sWt3473OikgnglnIILx/Jo= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e h1:f5mksnk+hgXHnImpZoWj64ja99j9zV7YUgrVG95uFE4= +golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go new file mode 100644 index 0000000..2f25334 --- /dev/null +++ b/internal/bridge/bridge.go @@ -0,0 +1,59 @@ +package bridge + +import ( + "encoding/json" + "net/http" + + "github.com/mumur/driver-booster/internal/drivers" + "github.com/mumur/driver-booster/internal/sysinfo" + "github.com/mumur/driver-booster/internal/winupdate" +) + +type Bridge struct { + DriverScanner *drivers.Scanner + UpdateChecker *winupdate.Checker + SysInfo *sysinfo.Collector +} + +func New(ds *drivers.Scanner, uc *winupdate.Checker, si *sysinfo.Collector) *Bridge { + return &Bridge{ + DriverScanner: ds, + UpdateChecker: uc, + SysInfo: si, + } +} + +func jsonResponse(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) +} + +func (b *Bridge) HandleSysInfo(w http.ResponseWriter, r *http.Request) { + info := b.SysInfo.Collect() + jsonResponse(w, info) +} + +func (b *Bridge) HandleDrivers(w http.ResponseWriter, r *http.Request) { + result := b.DriverScanner.Scan() + jsonResponse(w, result) +} + +func (b *Bridge) HandleDriverScan(w http.ResponseWriter, r *http.Request) { + result := b.DriverScanner.Scan() + jsonResponse(w, result) +} + +func (b *Bridge) HandleUpdates(w http.ResponseWriter, r *http.Request) { + result := b.UpdateChecker.Check() + jsonResponse(w, result) +} + +func (b *Bridge) HandleUpdateCheck(w http.ResponseWriter, r *http.Request) { + result := b.UpdateChecker.Check() + jsonResponse(w, result) +} + +func (b *Bridge) HandleUpdateInstall(w http.ResponseWriter, r *http.Request) { + result := b.UpdateChecker.Install(nil) + jsonResponse(w, result) +} diff --git a/internal/drivers/drivers.go b/internal/drivers/drivers.go new file mode 100644 index 0000000..61f0622 --- /dev/null +++ b/internal/drivers/drivers.go @@ -0,0 +1,153 @@ +package drivers + +import ( + "encoding/json" + "os/exec" + "strings" + "time" +) + +type Driver struct { + DeviceName string `json:"deviceName"` + DeviceClass string `json:"deviceClass"` + Manufacturer string `json:"manufacturer"` + DriverVersion string `json:"driverVersion"` + DriverDate string `json:"driverDate"` + Status string `json:"status"` + InfName string `json:"infName"` + IsSigned bool `json:"isSigned"` + NeedsUpdate bool `json:"needsUpdate"` +} + +type ScanResult struct { + Drivers []Driver `json:"drivers"` + TotalCount int `json:"totalCount"` + OutdatedCount int `json:"outdatedCount"` + ErrorCount int `json:"errorCount"` + ScanTime string `json:"scanTime"` + ScannedAt time.Time `json:"scannedAt"` +} + +type Scanner struct{} + +func NewScanner() *Scanner { + return &Scanner{} +} + +func (s *Scanner) Scan() ScanResult { + start := time.Now() + result := ScanResult{ + Drivers: []Driver{}, + } + + drivers := s.enumDriversPnP() + result.Drivers = drivers + result.TotalCount = len(drivers) + + for _, d := range drivers { + if d.NeedsUpdate { + result.OutdatedCount++ + } + if d.Status == "Error" || d.Status == "Degraded" { + result.ErrorCount++ + } + } + + result.ScanTime = time.Since(start).Round(time.Millisecond).String() + result.ScannedAt = time.Now() + return result +} + +func (s *Scanner) enumDriversPnP() []Driver { + // Use PowerShell to enumerate PnP signed drivers via WMI + psScript := ` +Get-CimInstance Win32_PnPSignedDriver | Where-Object { $_.DeviceName -ne $null } | Select-Object -First 100 DeviceName, DeviceClass, Manufacturer, DriverVersion, DriverDate, InfName, IsSigned, Status | ConvertTo-Json -Compress +` + out, err := exec.Command("powershell", "-NoProfile", "-Command", psScript).Output() + if err != nil { + return []Driver{} + } + + var raw []struct { + DeviceName string `json:"DeviceName"` + DeviceClass string `json:"DeviceClass"` + Manufacturer string `json:"Manufacturer"` + DriverVersion string `json:"DriverVersion"` + DriverDate string `json:"DriverDate"` + InfName string `json:"InfName"` + IsSigned bool `json:"IsSigned"` + Status string `json:"Status"` + } + + if err := json.Unmarshal(out, &raw); err != nil { + // Try as single object (when only 1 result) + var single struct { + DeviceName string `json:"DeviceName"` + DeviceClass string `json:"DeviceClass"` + Manufacturer string `json:"Manufacturer"` + DriverVersion string `json:"DriverVersion"` + DriverDate string `json:"DriverDate"` + InfName string `json:"InfName"` + IsSigned bool `json:"IsSigned"` + Status string `json:"Status"` + } + if err := json.Unmarshal(out, &single); err != nil { + return []Driver{} + } + raw = append(raw, single) + } + + drivers := make([]Driver, 0, len(raw)) + for _, r := range raw { + d := Driver{ + DeviceName: r.DeviceName, + DeviceClass: normalizeClass(r.DeviceClass), + Manufacturer: r.Manufacturer, + DriverVersion: r.DriverVersion, + DriverDate: formatDriverDate(r.DriverDate), + InfName: r.InfName, + IsSigned: r.IsSigned, + Status: r.Status, + } + d.NeedsUpdate = checkIfOutdated(r.DriverDate) + drivers = append(drivers, d) + } + + return drivers +} + +func normalizeClass(class string) string { + if class == "" { + return "Other" + } + return class +} + +func formatDriverDate(date string) string { + if date == "" { + return "Unknown" + } + // WMI returns dates like "20230115000000.000000-000" or "/Date(1234567890000)/" + if strings.Contains(date, "/Date(") { + return date // Will be parsed on frontend + } + if len(date) >= 8 { + return date[:4] + "-" + date[4:6] + "-" + date[6:8] + } + return date +} + +func checkIfOutdated(dateStr string) bool { + if dateStr == "" { + return false + } + // Consider drivers older than 2 years as potentially outdated + if len(dateStr) >= 8 { + t, err := time.Parse("20060102", dateStr[:8]) + if err != nil { + return false + } + return time.Since(t) > 2*365*24*time.Hour + } + return false +} diff --git a/internal/sysinfo/sysinfo.go b/internal/sysinfo/sysinfo.go new file mode 100644 index 0000000..56333a8 --- /dev/null +++ b/internal/sysinfo/sysinfo.go @@ -0,0 +1,156 @@ +package sysinfo + +import ( + "fmt" + "os/exec" + "runtime" + "strings" + "syscall" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGlobalMemoryEx = kernel32.NewProc("GlobalMemoryStatusEx") + procGetDiskFreeSpaceA = kernel32.NewProc("GetDiskFreeSpaceExA") + procGetComputerNameW = kernel32.NewProc("GetComputerNameW") + + ntdll = syscall.NewLazyDLL("ntdll.dll") + procRtlGetVersion = ntdll.NewProc("RtlGetVersion") +) + +type Info struct { + ComputerName string `json:"computerName"` + OSName string `json:"osName"` + OSVersion string `json:"osVersion"` + OSBuild string `json:"osBuild"` + Architecture string `json:"architecture"` + CPUName string `json:"cpuName"` + CPUCores int `json:"cpuCores"` + TotalRAM string `json:"totalRam"` + UsedRAM string `json:"usedRam"` + FreeRAM string `json:"freeRam"` + RAMPercent int `json:"ramPercent"` + DiskTotal string `json:"diskTotal"` + DiskFree string `json:"diskFree"` + DiskUsed string `json:"diskUsed"` + DiskPercent int `json:"diskPercent"` +} + +type memoryStatusEx struct { + Length uint32 + MemoryLoad uint32 + TotalPhys uint64 + AvailPhys uint64 + TotalPageFile uint64 + AvailPageFile uint64 + TotalVirtual uint64 + AvailVirtual uint64 + AvailExtendedVirtual uint64 +} + +type osVersionInfoExW struct { + OSVersionInfoSize uint32 + MajorVersion uint32 + MinorVersion uint32 + BuildNumber uint32 + PlatformId uint32 + CSDVersion [128]uint16 +} + +type Collector struct{} + +func NewCollector() *Collector { + return &Collector{} +} + +func (c *Collector) Collect() Info { + info := Info{ + Architecture: runtime.GOARCH, + CPUCores: runtime.NumCPU(), + } + + info.ComputerName = getComputerName() + info.CPUName = getCPUName() + + // OS version via RtlGetVersion (accurate, not subject to compatibility shims) + var osvi osVersionInfoExW + osvi.OSVersionInfoSize = uint32(unsafe.Sizeof(osvi)) + procRtlGetVersion.Call(uintptr(unsafe.Pointer(&osvi))) + info.OSVersion = fmt.Sprintf("%d.%d", osvi.MajorVersion, osvi.MinorVersion) + info.OSBuild = fmt.Sprintf("%d", osvi.BuildNumber) + info.OSName = getWindowsProductName() + + // Memory + var mem memoryStatusEx + mem.Length = uint32(unsafe.Sizeof(mem)) + procGlobalMemoryEx.Call(uintptr(unsafe.Pointer(&mem))) + info.TotalRAM = formatBytes(mem.TotalPhys) + info.FreeRAM = formatBytes(mem.AvailPhys) + info.UsedRAM = formatBytes(mem.TotalPhys - mem.AvailPhys) + info.RAMPercent = int(mem.MemoryLoad) + + // Disk (C: drive) + var freeBytesAvailable, totalBytes, totalFreeBytes uint64 + cDrive, _ := syscall.BytePtrFromString("C:\\") + procGetDiskFreeSpaceA.Call( + uintptr(unsafe.Pointer(cDrive)), + uintptr(unsafe.Pointer(&freeBytesAvailable)), + uintptr(unsafe.Pointer(&totalBytes)), + uintptr(unsafe.Pointer(&totalFreeBytes)), + ) + info.DiskTotal = formatBytes(totalBytes) + info.DiskFree = formatBytes(totalFreeBytes) + info.DiskUsed = formatBytes(totalBytes - totalFreeBytes) + if totalBytes > 0 { + info.DiskPercent = int(((totalBytes - totalFreeBytes) * 100) / totalBytes) + } + + return info +} + +func getComputerName() string { + var size uint32 = 256 + buf := make([]uint16, size) + procGetComputerNameW.Call(uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&size))) + return syscall.UTF16ToString(buf[:size]) +} + +func getCPUName() string { + out, err := exec.Command("powershell", "-NoProfile", "-Command", + "(Get-CimInstance Win32_Processor).Name").Output() + if err != nil { + return "Unknown CPU" + } + return strings.TrimSpace(string(out)) +} + +func getWindowsProductName() string { + out, err := exec.Command("powershell", "-NoProfile", "-Command", + "(Get-CimInstance Win32_OperatingSystem).Caption").Output() + if err != nil { + return "Windows" + } + return strings.TrimSpace(string(out)) +} + +func formatBytes(b uint64) string { + const ( + KB = 1024 + MB = KB * 1024 + GB = MB * 1024 + TB = GB * 1024 + ) + switch { + case b >= TB: + return fmt.Sprintf("%.1f TB", float64(b)/float64(TB)) + case b >= GB: + return fmt.Sprintf("%.1f GB", float64(b)/float64(GB)) + case b >= MB: + return fmt.Sprintf("%.1f MB", float64(b)/float64(MB)) + case b >= KB: + return fmt.Sprintf("%.1f KB", float64(b)/float64(KB)) + default: + return fmt.Sprintf("%d B", b) + } +} diff --git a/internal/winupdate/winupdate.go b/internal/winupdate/winupdate.go new file mode 100644 index 0000000..a918594 --- /dev/null +++ b/internal/winupdate/winupdate.go @@ -0,0 +1,208 @@ +package winupdate + +import ( + "encoding/json" + "os/exec" + "time" +) + +type Update struct { + Title string `json:"title"` + Description string `json:"description"` + KBArticle string `json:"kbArticle"` + Category string `json:"category"` + Size string `json:"size"` + IsInstalled bool `json:"isInstalled"` + IsDownloaded bool `json:"isDownloaded"` + IsMandatory bool `json:"isMandatory"` + Severity string `json:"severity"` +} + +type CheckResult struct { + Updates []Update `json:"updates"` + PendingCount int `json:"pendingCount"` + InstalledCount int `json:"installedCount"` + CheckTime string `json:"checkTime"` + CheckedAt time.Time `json:"checkedAt"` + Error string `json:"error,omitempty"` +} + +type InstallResult struct { + Success bool `json:"success"` + Message string `json:"message"` +} + +type Checker struct{} + +func NewChecker() *Checker { + return &Checker{} +} + +func (c *Checker) Check() CheckResult { + start := time.Now() + result := CheckResult{ + Updates: []Update{}, + } + + // Use PowerShell COM object for Windows Update Agent + psScript := ` +$ErrorActionPreference = 'SilentlyContinue' +$Session = New-Object -ComObject Microsoft.Update.Session +$Searcher = $Session.CreateUpdateSearcher() + +# Get installed update history (recent 50) +$HistoryCount = $Searcher.GetTotalHistoryCount() +$History = $Searcher.QueryHistory(0, [Math]::Min($HistoryCount, 50)) + +$installed = @() +foreach ($entry in $History) { + if ($entry.Title) { + $installed += @{ + Title = $entry.Title + Description = $entry.Description + Date = $entry.Date.ToString("yyyy-MM-dd") + ResultCode = $entry.ResultCode + } + } +} + +# Search for pending updates +try { + $SearchResult = $Searcher.Search("IsInstalled=0") + $pending = @() + foreach ($update in $SearchResult.Updates) { + $kb = "" + if ($update.KBArticleIDs.Count -gt 0) { $kb = "KB" + $update.KBArticleIDs.Item(0) } + $cat = "" + if ($update.Categories.Count -gt 0) { $cat = $update.Categories.Item(0).Name } + $sev = if ($update.MsrcSeverity) { $update.MsrcSeverity } else { "Unspecified" } + $sz = "" + if ($update.MaxDownloadSize -gt 0) { + $mb = [Math]::Round($update.MaxDownloadSize / 1MB, 1) + $sz = "$mb MB" + } + $pending += @{ + Title = $update.Title + Description = $update.Description + KBArticle = $kb + Category = $cat + Size = $sz + IsDownloaded = $update.IsDownloaded + IsMandatory = $update.IsMandatory + Severity = $sev + } + } +} catch { + $pending = @() +} + +@{ + Installed = $installed + Pending = $pending +} | ConvertTo-Json -Depth 3 -Compress +` + out, err := exec.Command("powershell", "-NoProfile", "-Command", psScript).Output() + if err != nil { + result.Error = "Failed to check updates: " + err.Error() + result.CheckTime = time.Since(start).Round(time.Millisecond).String() + result.CheckedAt = time.Now() + return result + } + + var raw struct { + Installed []struct { + Title string `json:"Title"` + Description string `json:"Description"` + Date string `json:"Date"` + ResultCode int `json:"ResultCode"` + } `json:"Installed"` + Pending []struct { + Title string `json:"Title"` + Description string `json:"Description"` + KBArticle string `json:"KBArticle"` + Category string `json:"Category"` + Size string `json:"Size"` + IsDownloaded bool `json:"IsDownloaded"` + IsMandatory bool `json:"IsMandatory"` + Severity string `json:"Severity"` + } `json:"Pending"` + } + + if err := json.Unmarshal(out, &raw); err != nil { + result.Error = "Failed to parse update data" + result.CheckTime = time.Since(start).Round(time.Millisecond).String() + result.CheckedAt = time.Now() + return result + } + + for _, p := range raw.Pending { + result.Updates = append(result.Updates, Update{ + Title: p.Title, + Description: p.Description, + KBArticle: p.KBArticle, + Category: p.Category, + Size: p.Size, + IsInstalled: false, + IsDownloaded: p.IsDownloaded, + IsMandatory: p.IsMandatory, + Severity: p.Severity, + }) + result.PendingCount++ + } + + for _, i := range raw.Installed { + result.Updates = append(result.Updates, Update{ + Title: i.Title, + Description: i.Description, + IsInstalled: true, + Category: "Installed", + }) + result.InstalledCount++ + } + + result.CheckTime = time.Since(start).Round(time.Millisecond).String() + result.CheckedAt = time.Now() + return result +} + +func (c *Checker) Install(titles []string) InstallResult { + // Trigger Windows Update installation via PowerShell + psScript := ` +$Session = New-Object -ComObject Microsoft.Update.Session +$Searcher = $Session.CreateUpdateSearcher() +$SearchResult = $Searcher.Search("IsInstalled=0") +$ToInstall = New-Object -ComObject Microsoft.Update.UpdateColl + +foreach ($update in $SearchResult.Updates) { + if ($update.IsDownloaded -eq $false) { + $Downloader = $Session.CreateUpdateDownloader() + $dl = New-Object -ComObject Microsoft.Update.UpdateColl + $dl.Add($update) | Out-Null + $Downloader.Updates = $dl + $Downloader.Download() | Out-Null + } + $ToInstall.Add($update) | Out-Null +} + +if ($ToInstall.Count -gt 0) { + $Installer = $Session.CreateUpdateInstaller() + $Installer.Updates = $ToInstall + $Result = $Installer.Install() + Write-Output "Installed $($ToInstall.Count) updates. Result: $($Result.ResultCode)" +} else { + Write-Output "No updates to install." +} +` + out, err := exec.Command("powershell", "-NoProfile", "-Command", psScript).Output() + if err != nil { + return InstallResult{ + Success: false, + Message: "Installation failed: " + err.Error(), + } + } + + return InstallResult{ + Success: true, + Message: string(out), + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..5e53dcf --- /dev/null +++ b/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "embed" + "encoding/json" + "fmt" + "io/fs" + "log" + "net" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/jchv/go-webview2" + "github.com/mumur/driver-booster/internal/bridge" + "github.com/mumur/driver-booster/internal/drivers" + "github.com/mumur/driver-booster/internal/sysinfo" + "github.com/mumur/driver-booster/internal/winupdate" +) + +//go:embed ui/* +var uiFS embed.FS + +func main() { + // Start embedded HTTP server for UI assets + uiContent, err := fs.Sub(uiFS, "ui") + if err != nil { + log.Fatal("Failed to load UI assets:", err) + } + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + log.Fatal("Failed to start UI server:", err) + } + port := listener.Addr().(*net.TCPAddr).Port + + mux := http.NewServeMux() + mux.Handle("/", http.FileServer(http.FS(uiContent))) + + // API endpoints + b := bridge.New( + drivers.NewScanner(), + winupdate.NewChecker(), + sysinfo.NewCollector(), + ) + mux.HandleFunc("/api/sysinfo", b.HandleSysInfo) + mux.HandleFunc("/api/drivers", b.HandleDrivers) + mux.HandleFunc("/api/drivers/scan", b.HandleDriverScan) + mux.HandleFunc("/api/updates", b.HandleUpdates) + mux.HandleFunc("/api/updates/check", b.HandleUpdateCheck) + mux.HandleFunc("/api/updates/install", b.HandleUpdateInstall) + + go func() { + if err := http.Serve(listener, mux); err != nil { + log.Println("HTTP server error:", err) + } + }() + + // Create WebView2 window + w := webview2.NewWithOptions(webview2.WebViewOptions{ + Debug: false, + AutoFocus: true, + WindowOptions: webview2.WindowOptions{ + Title: "Driver Booster Pro", + Width: 1100, + Height: 720, + IconId: 2, + Center: true, + }, + }) + if w == nil { + log.Fatal("Failed to create WebView2 window. Make sure WebView2 runtime is installed.") + } + defer w.Destroy() + + // Bind Go functions to JS + w.Bind("goGetSysInfo", func() string { + info := b.SysInfo.Collect() + data, _ := json.Marshal(info) + return string(data) + }) + w.Bind("goScanDrivers", func() string { + result := b.DriverScanner.Scan() + data, _ := json.Marshal(result) + return string(data) + }) + w.Bind("goCheckUpdates", func() string { + result := b.UpdateChecker.Check() + data, _ := json.Marshal(result) + return string(data) + }) + + w.Navigate(fmt.Sprintf("http://127.0.0.1:%d/index.html", port)) + w.Run() + + // Cleanup + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigCh + w.Destroy() + os.Exit(0) + }() +} diff --git a/ui/app.js b/ui/app.js new file mode 100644 index 0000000..4dc4c24 --- /dev/null +++ b/ui/app.js @@ -0,0 +1,252 @@ +const App = { + state: { + sysInfo: null, + drivers: null, + updates: null, + }, + + init() { + this.setupNavigation(); + this.setupFilters(); + this.refreshSysInfo(); + }, + + setupNavigation() { + document.querySelectorAll('.nav-item').forEach(item => { + item.addEventListener('click', () => { + const page = item.dataset.page; + document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); + item.classList.add('active'); + document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); + document.getElementById('page-' + page).classList.add('active'); + }); + }); + }, + + setupFilters() { + document.querySelectorAll('.filter-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + this.filterDrivers(btn.dataset.filter); + }); + }); + }, + + filterDrivers(filter) { + document.querySelectorAll('.driver-card').forEach(card => { + const show = filter === 'all' || + (filter === 'outdated' && card.classList.contains('outdated')) || + (filter === 'error' && card.classList.contains('error')) || + (filter === 'signed' && card.dataset.signed === 'true'); + card.style.display = show ? '' : 'none'; + }); + }, + + showLoading(text) { + document.getElementById('loading-text').textContent = text; + document.getElementById('loading-overlay').style.display = 'flex'; + }, + + hideLoading() { + document.getElementById('loading-overlay').style.display = 'none'; + }, + + // System Info + async refreshSysInfo() { + this.showLoading('Collecting system information...'); + try { + const res = await fetch('/api/sysinfo'); + const info = await res.json(); + this.state.sysInfo = info; + this.renderSysInfo(info); + this.updateDashboardResources(info); + } catch (e) { + console.error('Failed to get sysinfo:', e); + } + this.hideLoading(); + }, + + renderSysInfo(info) { + document.getElementById('sys-name').textContent = info.computerName || '--'; + document.getElementById('sys-os').textContent = info.osName || '--'; + document.getElementById('sys-version').textContent = info.osVersion || '--'; + document.getElementById('sys-build').textContent = info.osBuild || '--'; + document.getElementById('sys-arch').textContent = info.architecture || '--'; + document.getElementById('sys-cpu').textContent = info.cpuName || '--'; + document.getElementById('sys-cores').textContent = info.cpuCores || '--'; + document.getElementById('sys-ram-total').textContent = info.totalRam || '--'; + document.getElementById('sys-ram-used').textContent = info.usedRam || '--'; + document.getElementById('sys-ram-free').textContent = info.freeRam || '--'; + document.getElementById('sys-ram-pct').textContent = (info.ramPercent || 0) + '%'; + document.getElementById('sys-disk-total').textContent = info.diskTotal || '--'; + document.getElementById('sys-disk-used').textContent = info.diskUsed || '--'; + document.getElementById('sys-disk-free').textContent = info.diskFree || '--'; + document.getElementById('sys-disk-pct').textContent = (info.diskPercent || 0) + '%'; + }, + + updateDashboardResources(info) { + document.getElementById('dash-ram-percent').textContent = (info.ramPercent || 0) + '%'; + document.getElementById('dash-ram-detail').textContent = (info.usedRam || '--') + ' / ' + (info.totalRam || '--'); + document.getElementById('dash-ram-bar').style.width = (info.ramPercent || 0) + '%'; + document.getElementById('dash-disk-detail').textContent = (info.diskUsed || '--') + ' / ' + (info.diskTotal || '--'); + document.getElementById('dash-disk-bar').style.width = (info.diskPercent || 0) + '%'; + + if (info.ramPercent > 80) { + document.getElementById('dash-ram-bar').style.background = 'linear-gradient(90deg, #f59e0b, #ef4444)'; + } + if (info.diskPercent > 85) { + document.getElementById('dash-disk-bar').style.background = 'linear-gradient(90deg, #f59e0b, #ef4444)'; + } + }, + + // Drivers + async scanDrivers() { + this.showLoading('Scanning drivers... This may take a moment.'); + try { + const res = await fetch('/api/drivers/scan'); + const result = await res.json(); + this.state.drivers = result; + this.renderDrivers(result); + this.updateDashboardDrivers(result); + } catch (e) { + console.error('Failed to scan drivers:', e); + } + this.hideLoading(); + }, + + renderDrivers(result) { + document.getElementById('driver-summary').style.display = 'flex'; + document.getElementById('driver-filters').style.display = 'flex'; + document.getElementById('drv-total').textContent = result.totalCount; + document.getElementById('drv-outdated').textContent = result.outdatedCount; + document.getElementById('drv-errors').textContent = result.errorCount; + document.getElementById('drv-time').textContent = result.scanTime; + + const list = document.getElementById('driver-list'); + if (!result.drivers || result.drivers.length === 0) { + list.innerHTML = '

No drivers found.

'; + return; + } + + list.innerHTML = result.drivers.map(d => { + const classes = ['driver-card']; + if (d.needsUpdate) classes.push('outdated'); + if (d.status === 'Error' || d.status === 'Degraded') classes.push('error'); + + const icon = this.getClassIcon(d.deviceClass); + const badges = []; + if (d.isSigned) badges.push('Signed'); + else badges.push('Unsigned'); + if (d.needsUpdate) badges.push('Outdated'); + else badges.push('OK'); + + return '
' + + '
' + icon + '
' + + '
' + + '
' + this.esc(d.deviceName) + '
' + + '
' + this.esc(d.manufacturer || 'Unknown') + ' • v' + this.esc(d.driverVersion || '?') + ' • ' + this.esc(d.driverDate) + '
' + + '
' + + '
' + badges.join('') + '
' + + '
'; + }).join(''); + }, + + updateDashboardDrivers(result) { + document.getElementById('dash-driver-count').textContent = result.totalCount; + document.getElementById('dash-outdated-count').textContent = result.outdatedCount; + }, + + getClassIcon(cls) { + const icons = { + 'DISPLAY': '\uD83D\uDDA5', + 'MEDIA': '\uD83D\uDD0A', + 'NET': '\uD83C\uDF10', + 'USB': '\uD83D\uDD0C', + 'HIDCLASS': '\uD83D\uDDB1', + 'KEYBOARD': '\u2328', + 'DISKDRIVE': '\uD83D\uDCBE', + 'PROCESSOR': '\u26A1', + 'SYSTEM': '\u2699', + 'BLUETOOTH': '\uD83D\uDCE1', + 'CAMERA': '\uD83D\uDCF7', + 'PRINTER': '\uD83D\uDDA8', + }; + if (!cls) return '\uD83D\uDCE6'; + var upper = cls.toUpperCase(); + for (var key in icons) { + if (upper.indexOf(key) !== -1) return icons[key]; + } + return '\uD83D\uDCE6'; + }, + + // Updates + async checkUpdates() { + this.showLoading('Checking for Windows updates...'); + try { + const res = await fetch('/api/updates/check'); + const result = await res.json(); + this.state.updates = result; + this.renderUpdates(result); + document.getElementById('dash-update-count').textContent = result.pendingCount; + } catch (e) { + console.error('Failed to check updates:', e); + } + this.hideLoading(); + }, + + renderUpdates(result) { + const list = document.getElementById('update-list'); + + if (result.error) { + list.innerHTML = '

' + this.esc(result.error) + '

'; + return; + } + + if (!result.updates || result.updates.length === 0) { + list.innerHTML = '

Your system is up to date!

'; + return; + } + + const pending = result.updates.filter(u => !u.isInstalled); + const installed = result.updates.filter(u => u.isInstalled); + + let html = ''; + + if (pending.length > 0) { + html += '

Pending Updates (' + pending.length + ')

'; + html += pending.map(u => + '
' + + '
' + this.esc(u.title) + '
' + + '
' + + (u.kbArticle ? '' + this.esc(u.kbArticle) + '' : '') + + (u.category ? '' + this.esc(u.category) + '' : '') + + (u.size ? '' + this.esc(u.size) + '' : '') + + (u.severity && u.severity !== 'Unspecified' ? '' + this.esc(u.severity) + '' : '') + + (u.isMandatory ? 'Mandatory' : '') + + '
' + + '
' + ).join(''); + } + + if (installed.length > 0) { + html += '

Recently Installed (' + installed.length + ')

'; + html += installed.map(u => + '
' + + '
' + this.esc(u.title) + '
' + + '
' + ).join(''); + } + + list.innerHTML = html; + }, + + esc(str) { + if (!str) return ''; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + }, +}; + +document.addEventListener('DOMContentLoaded', function() { App.init(); }); diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000..b952866 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,243 @@ + + + + + + Driver Booster Pro + + + +
+ + + + +
+ +
+ +
+
+
+ +
+
+ -- + Total Drivers +
+
+
+
+ +
+
+ -- + Outdated Drivers +
+
+
+
+ +
+
+ -- + Pending Updates +
+
+
+
+ +
+
+ -- + RAM Usage +
+
+
+ +
+
+
+

Quick Actions

+
+
+ + + +
+
+
+
+

System Resources

+
+
+
+
+ RAM + -- / -- +
+
+
+
+
+
+
+ Disk (C:) + -- / -- +
+
+
+
+
+
+
+
+
+ + +
+ + + +
+
+ +

Click Scan Now to detect installed drivers

+
+
+
+ + +
+ +
+
+ +

Click Check Now to search for Windows updates

+
+
+
+ + +
+ +
+
+

Computer

+
Name--
+
OS--
+
Version--
+
Build--
+
Architecture--
+
+
+

Processor

+
CPU--
+
Cores--
+
+
+

Memory

+
Total--
+
Used--
+
Free--
+
Usage--
+
+
+

Storage (C:)

+
Total--
+
Used--
+
Free--
+
Usage--
+
+
+
+
+
+ + + + + + + diff --git a/ui/style.css b/ui/style.css new file mode 100644 index 0000000..2256af9 --- /dev/null +++ b/ui/style.css @@ -0,0 +1,424 @@ +:root { + --bg-primary: #0f0f1a; + --bg-secondary: #1a1a2e; + --bg-card: #16213e; + --bg-hover: #1f2b4d; + --border: #2a2a4a; + --text-primary: #e8e8f0; + --text-secondary: #9ca3af; + --text-muted: #6b7280; + --accent-primary: #6366f1; + --accent-secondary: #8b5cf6; + --accent-glow: rgba(99, 102, 241, 0.15); + --success: #10b981; + --warning: #f59e0b; + --danger: #ef4444; + --info: #3b82f6; + --radius: 12px; + --radius-sm: 8px; + --sidebar-width: 240px; + --transition: 0.2s ease; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + overflow: hidden; + height: 100vh; + -webkit-font-smoothing: antialiased; +} + +.app { display: flex; height: 100vh; } + +/* Sidebar */ +.sidebar { + width: var(--sidebar-width); + background: var(--bg-secondary); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.sidebar-header { padding: 20px; border-bottom: 1px solid var(--border); } + +.logo { display: flex; align-items: center; gap: 10px; } + +.logo-text { + font-size: 16px; + font-weight: 700; + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.nav-items { list-style: none; padding: 12px; flex: 1; } + +.nav-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border-radius: var(--radius-sm); + cursor: pointer; + color: var(--text-secondary); + transition: all var(--transition); + margin-bottom: 4px; + font-size: 14px; + user-select: none; +} + +.nav-item:hover { background: var(--bg-hover); color: var(--text-primary); } + +.nav-item.active { + background: var(--accent-glow); + color: var(--accent-primary); + font-weight: 600; +} + +.nav-item.active svg { stroke: var(--accent-primary); } + +.sidebar-footer { padding: 16px 20px; border-top: 1px solid var(--border); } + +.version-badge { + font-size: 11px; + color: var(--text-muted); + background: var(--bg-primary); + padding: 4px 10px; + border-radius: 20px; + display: inline-block; +} + +/* Content */ +.content { + flex: 1; + overflow-y: auto; + padding: 32px; + background: var(--bg-primary); +} + +.content::-webkit-scrollbar { width: 6px; } +.content::-webkit-scrollbar-track { background: transparent; } +.content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } + +.page { display: none; animation: fadeIn 0.3s ease; } +.page.active { display: block; } + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.page-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 28px; +} + +.page-header h1 { font-size: 24px; font-weight: 700; letter-spacing: -0.5px; } + +.subtitle { color: var(--text-secondary); font-size: 14px; margin-top: 4px; } + +/* Stats Grid */ +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; + display: flex; + align-items: center; + gap: 16px; + transition: all var(--transition); +} + +.stat-card:hover { + border-color: var(--accent-primary); + box-shadow: 0 0 20px var(--accent-glow); + transform: translateY(-2px); +} + +.stat-icon { + width: 48px; + height: 48px; + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.drivers-icon { background: rgba(99, 102, 241, 0.15); color: var(--accent-primary); } +.outdated-icon { background: rgba(245, 158, 11, 0.15); color: var(--warning); } +.updates-icon { background: rgba(59, 130, 246, 0.15); color: var(--info); } +.ram-icon { background: rgba(16, 185, 129, 0.15); color: var(--success); } + +.stat-value { font-size: 28px; font-weight: 700; display: block; line-height: 1.1; } +.stat-label { font-size: 12px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; } + +/* Panels */ +.dashboard-panels { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } + +.panel { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +.panel-header { padding: 16px 20px; border-bottom: 1px solid var(--border); } + +.panel-header h3 { + font-size: 14px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Quick Actions */ +.quick-actions { padding: 20px; display: flex; flex-direction: column; gap: 10px; } + +.action-btn { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 20px; + border: none; + border-radius: var(--radius-sm); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition); + font-family: inherit; +} + +.action-btn.primary { + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); + color: white; +} + +.action-btn.primary:hover { + box-shadow: 0 4px 16px rgba(99, 102, 241, 0.4); + transform: translateY(-1px); +} + +.action-btn.secondary { + background: rgba(59, 130, 246, 0.15); + color: var(--info); + border: 1px solid rgba(59, 130, 246, 0.3); +} + +.action-btn.secondary:hover { background: rgba(59, 130, 246, 0.25); } + +.action-btn.accent { + background: rgba(16, 185, 129, 0.15); + color: var(--success); + border: 1px solid rgba(16, 185, 129, 0.3); +} + +.action-btn.accent:hover { background: rgba(16, 185, 129, 0.25); } + +.action-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; } + +/* Resource Bars */ +.resource-bars { padding: 20px; display: flex; flex-direction: column; gap: 16px; } + +.resource-label { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; } +.resource-label span:first-child { font-weight: 600; } +.resource-label span:last-child { color: var(--text-secondary); } + +.progress-bar { height: 8px; background: var(--bg-primary); border-radius: 4px; overflow: hidden; } + +.progress-fill { height: 100%; border-radius: 4px; transition: width 0.8s ease; } +.ram-fill { background: linear-gradient(90deg, var(--success), #34d399); } +.disk-fill { background: linear-gradient(90deg, var(--info), #60a5fa); } + +/* Driver List */ +.driver-summary { + display: flex; + gap: 24px; + margin-bottom: 16px; + padding: 14px 20px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} + +.summary-item { font-size: 13px; color: var(--text-secondary); } +.summary-item strong { color: var(--text-primary); font-size: 16px; margin-right: 4px; } +.summary-item.warning strong { color: var(--warning); } +.summary-item.error strong { color: var(--danger); } + +.filter-bar { display: flex; gap: 8px; margin-bottom: 16px; } + +.filter-btn { + padding: 6px 16px; + border: 1px solid var(--border); + border-radius: 20px; + background: transparent; + color: var(--text-secondary); + font-size: 13px; + cursor: pointer; + transition: all var(--transition); + font-family: inherit; +} + +.filter-btn:hover { border-color: var(--accent-primary); color: var(--accent-primary); } + +.filter-btn.active { + background: var(--accent-primary); + border-color: var(--accent-primary); + color: white; +} + +.driver-list, .update-list { display: flex; flex-direction: column; gap: 8px; } + +.driver-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 16px 20px; + display: flex; + align-items: center; + gap: 16px; + transition: all var(--transition); +} + +.driver-card:hover { border-color: var(--accent-primary); background: var(--bg-hover); } +.driver-card.outdated { border-left: 3px solid var(--warning); } +.driver-card.error { border-left: 3px solid var(--danger); } + +.driver-class-icon { + width: 40px; + height: 40px; + border-radius: var(--radius-sm); + background: rgba(99, 102, 241, 0.1); + display: flex; + align-items: center; + justify-content: center; + color: var(--accent-primary); + font-size: 18px; + flex-shrink: 0; +} + +.driver-info { flex: 1; min-width: 0; } + +.driver-name { + font-weight: 600; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.driver-meta { font-size: 12px; color: var(--text-secondary); margin-top: 2px; } +.driver-badges { display: flex; gap: 6px; flex-shrink: 0; } + +.badge { font-size: 11px; padding: 3px 10px; border-radius: 20px; font-weight: 600; } +.badge-signed { background: rgba(16, 185, 129, 0.15); color: var(--success); } +.badge-unsigned { background: rgba(239, 68, 68, 0.15); color: var(--danger); } +.badge-outdated { background: rgba(245, 158, 11, 0.15); color: var(--warning); } +.badge-ok { background: rgba(16, 185, 129, 0.15); color: var(--success); } + +/* Update Cards */ +.update-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 16px 20px; + transition: all var(--transition); +} + +.update-card:hover { border-color: var(--accent-primary); } +.update-card.installed { opacity: 0.6; } +.update-card.pending { border-left: 3px solid var(--info); } + +.update-title { font-weight: 600; font-size: 14px; margin-bottom: 4px; } +.update-meta { font-size: 12px; color: var(--text-secondary); display: flex; gap: 16px; flex-wrap: wrap; } + +/* System Info Grid */ +.sysinfo-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; } + +.info-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; +} + +.info-card h4 { + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--accent-primary); + margin-bottom: 16px; + padding-bottom: 10px; + border-bottom: 1px solid var(--border); +} + +.info-row { + display: flex; + justify-content: space-between; + padding: 8px 0; + font-size: 13px; + border-bottom: 1px solid rgba(42, 42, 74, 0.5); +} + +.info-row:last-child { border-bottom: none; } +.info-key { color: var(--text-secondary); } + +.info-val { + font-weight: 600; + text-align: right; + max-width: 60%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Empty State */ +.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); } +.empty-state p { margin-top: 16px; font-size: 14px; } + +/* Loading Overlay */ +.loading-overlay { + position: fixed; + inset: 0; + background: rgba(15, 15, 26, 0.85); + backdrop-filter: blur(4px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid var(--border); + border-top-color: var(--accent-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { to { transform: rotate(360deg); } } + +.loading-overlay p { margin-top: 16px; color: var(--text-secondary); font-size: 14px; } + +@media (max-width: 900px) { + .stats-grid { grid-template-columns: repeat(2, 1fr); } + .dashboard-panels { grid-template-columns: 1fr; } + .sysinfo-grid { grid-template-columns: 1fr; } +}