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) <noreply@anthropic.com>
This commit is contained in:
taqin
2026-04-12 20:42:34 +07:00
commit 3e432fbb50
11 changed files with 1638 additions and 0 deletions

22
.gitignore vendored Normal file
View File

@@ -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

9
go.mod Normal file
View File

@@ -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
)

7
go.sum Normal file
View File

@@ -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=

59
internal/bridge/bridge.go Normal file
View File

@@ -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)
}

153
internal/drivers/drivers.go Normal file
View File

@@ -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
}

156
internal/sysinfo/sysinfo.go Normal file
View File

@@ -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)
}
}

View File

@@ -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),
}
}

105
main.go Normal file
View File

@@ -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)
}()
}

252
ui/app.js Normal file
View File

@@ -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 = '<div class="empty-state"><p>No drivers found.</p></div>';
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('<span class="badge badge-signed">Signed</span>');
else badges.push('<span class="badge badge-unsigned">Unsigned</span>');
if (d.needsUpdate) badges.push('<span class="badge badge-outdated">Outdated</span>');
else badges.push('<span class="badge badge-ok">OK</span>');
return '<div class="' + classes.join(' ') + '" data-signed="' + d.isSigned + '">' +
'<div class="driver-class-icon">' + icon + '</div>' +
'<div class="driver-info">' +
'<div class="driver-name">' + this.esc(d.deviceName) + '</div>' +
'<div class="driver-meta">' + this.esc(d.manufacturer || 'Unknown') + ' &bull; v' + this.esc(d.driverVersion || '?') + ' &bull; ' + this.esc(d.driverDate) + '</div>' +
'</div>' +
'<div class="driver-badges">' + badges.join('') + '</div>' +
'</div>';
}).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 = '<div class="empty-state"><p style="color:var(--danger)">' + this.esc(result.error) + '</p></div>';
return;
}
if (!result.updates || result.updates.length === 0) {
list.innerHTML = '<div class="empty-state"><p>Your system is up to date!</p></div>';
return;
}
const pending = result.updates.filter(u => !u.isInstalled);
const installed = result.updates.filter(u => u.isInstalled);
let html = '';
if (pending.length > 0) {
html += '<h3 style="margin:8px 0;font-size:14px;color:var(--info)">Pending Updates (' + pending.length + ')</h3>';
html += pending.map(u =>
'<div class="update-card pending">' +
'<div class="update-title">' + this.esc(u.title) + '</div>' +
'<div class="update-meta">' +
(u.kbArticle ? '<span>' + this.esc(u.kbArticle) + '</span>' : '') +
(u.category ? '<span>' + this.esc(u.category) + '</span>' : '') +
(u.size ? '<span>' + this.esc(u.size) + '</span>' : '') +
(u.severity && u.severity !== 'Unspecified' ? '<span>' + this.esc(u.severity) + '</span>' : '') +
(u.isMandatory ? '<span style="color:var(--danger)">Mandatory</span>' : '') +
'</div>' +
'</div>'
).join('');
}
if (installed.length > 0) {
html += '<h3 style="margin:16px 0 8px;font-size:14px;color:var(--success)">Recently Installed (' + installed.length + ')</h3>';
html += installed.map(u =>
'<div class="update-card installed">' +
'<div class="update-title">' + this.esc(u.title) + '</div>' +
'</div>'
).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(); });

243
ui/index.html Normal file
View File

@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Driver Booster Pro</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="app">
<!-- Sidebar -->
<nav class="sidebar">
<div class="sidebar-header">
<div class="logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="url(#logo-grad)"/>
<path d="M8 16L14 22L24 10" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
<defs><linearGradient id="logo-grad" x1="0" y1="0" x2="32" y2="32"><stop stop-color="#6366f1"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient></defs>
</svg>
<span class="logo-text">Driver Booster</span>
</div>
</div>
<ul class="nav-items">
<li class="nav-item active" data-page="dashboard">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
<span>Dashboard</span>
</li>
<li class="nav-item" data-page="drivers">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
<span>Drivers</span>
</li>
<li class="nav-item" data-page="updates">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-6.219-8.56"/><polyline points="21 3 21 9 15 9"/></svg>
<span>Windows Update</span>
</li>
<li class="nav-item" data-page="system">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
<span>System Info</span>
</li>
</ul>
<div class="sidebar-footer">
<div class="version-badge">v1.0.0</div>
</div>
</nav>
<!-- Main Content -->
<main class="content">
<!-- Dashboard Page -->
<div class="page active" id="page-dashboard">
<div class="page-header">
<h1>Dashboard</h1>
<p class="subtitle">System health overview</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon drivers-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
</div>
<div class="stat-info">
<span class="stat-value" id="dash-driver-count">--</span>
<span class="stat-label">Total Drivers</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon outdated-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<div class="stat-info">
<span class="stat-value" id="dash-outdated-count">--</span>
<span class="stat-label">Outdated Drivers</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon updates-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-6.219-8.56"/><polyline points="21 3 21 9 15 9"/></svg>
</div>
<div class="stat-info">
<span class="stat-value" id="dash-update-count">--</span>
<span class="stat-label">Pending Updates</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon ram-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/></svg>
</div>
<div class="stat-info">
<span class="stat-value" id="dash-ram-percent">--</span>
<span class="stat-label">RAM Usage</span>
</div>
</div>
</div>
<div class="dashboard-panels">
<div class="panel">
<div class="panel-header">
<h3>Quick Actions</h3>
</div>
<div class="quick-actions">
<button class="action-btn primary" onclick="App.scanDrivers()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
Scan Drivers
</button>
<button class="action-btn secondary" onclick="App.checkUpdates()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-6.219-8.56"/><polyline points="21 3 21 9 15 9"/></svg>
Check Updates
</button>
<button class="action-btn accent" onclick="App.refreshSysInfo()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
Refresh System
</button>
</div>
</div>
<div class="panel">
<div class="panel-header">
<h3>System Resources</h3>
</div>
<div class="resource-bars">
<div class="resource-item">
<div class="resource-label">
<span>RAM</span>
<span id="dash-ram-detail">-- / --</span>
</div>
<div class="progress-bar">
<div class="progress-fill ram-fill" id="dash-ram-bar" style="width: 0%"></div>
</div>
</div>
<div class="resource-item">
<div class="resource-label">
<span>Disk (C:)</span>
<span id="dash-disk-detail">-- / --</span>
</div>
<div class="progress-bar">
<div class="progress-fill disk-fill" id="dash-disk-bar" style="width: 0%"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Drivers Page -->
<div class="page" id="page-drivers">
<div class="page-header">
<h1>Driver Manager</h1>
<div class="page-actions">
<button class="action-btn primary" onclick="App.scanDrivers()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
Scan Now
</button>
</div>
</div>
<div class="driver-summary" id="driver-summary" style="display:none">
<div class="summary-item"><strong id="drv-total">0</strong> Total</div>
<div class="summary-item warning"><strong id="drv-outdated">0</strong> Outdated</div>
<div class="summary-item error"><strong id="drv-errors">0</strong> Errors</div>
<div class="summary-item muted">Scanned in <span id="drv-time">--</span></div>
</div>
<div class="filter-bar" id="driver-filters" style="display:none">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="outdated">Outdated</button>
<button class="filter-btn" data-filter="error">Errors</button>
<button class="filter-btn" data-filter="signed">Signed</button>
</div>
<div id="driver-list" class="driver-list">
<div class="empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.3"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
<p>Click <strong>Scan Now</strong> to detect installed drivers</p>
</div>
</div>
</div>
<!-- Updates Page -->
<div class="page" id="page-updates">
<div class="page-header">
<h1>Windows Update</h1>
<div class="page-actions">
<button class="action-btn primary" onclick="App.checkUpdates()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-6.219-8.56"/><polyline points="21 3 21 9 15 9"/></svg>
Check Now
</button>
</div>
</div>
<div id="update-list" class="update-list">
<div class="empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.3"><path d="M21 12a9 9 0 11-6.219-8.56"/><polyline points="21 3 21 9 15 9"/></svg>
<p>Click <strong>Check Now</strong> to search for Windows updates</p>
</div>
</div>
</div>
<!-- System Info Page -->
<div class="page" id="page-system">
<div class="page-header">
<h1>System Information</h1>
<div class="page-actions">
<button class="action-btn primary" onclick="App.refreshSysInfo()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>
Refresh
</button>
</div>
</div>
<div class="sysinfo-grid" id="sysinfo-grid">
<div class="info-card">
<h4>Computer</h4>
<div class="info-row"><span class="info-key">Name</span><span class="info-val" id="sys-name">--</span></div>
<div class="info-row"><span class="info-key">OS</span><span class="info-val" id="sys-os">--</span></div>
<div class="info-row"><span class="info-key">Version</span><span class="info-val" id="sys-version">--</span></div>
<div class="info-row"><span class="info-key">Build</span><span class="info-val" id="sys-build">--</span></div>
<div class="info-row"><span class="info-key">Architecture</span><span class="info-val" id="sys-arch">--</span></div>
</div>
<div class="info-card">
<h4>Processor</h4>
<div class="info-row"><span class="info-key">CPU</span><span class="info-val" id="sys-cpu">--</span></div>
<div class="info-row"><span class="info-key">Cores</span><span class="info-val" id="sys-cores">--</span></div>
</div>
<div class="info-card">
<h4>Memory</h4>
<div class="info-row"><span class="info-key">Total</span><span class="info-val" id="sys-ram-total">--</span></div>
<div class="info-row"><span class="info-key">Used</span><span class="info-val" id="sys-ram-used">--</span></div>
<div class="info-row"><span class="info-key">Free</span><span class="info-val" id="sys-ram-free">--</span></div>
<div class="info-row"><span class="info-key">Usage</span><span class="info-val" id="sys-ram-pct">--</span></div>
</div>
<div class="info-card">
<h4>Storage (C:)</h4>
<div class="info-row"><span class="info-key">Total</span><span class="info-val" id="sys-disk-total">--</span></div>
<div class="info-row"><span class="info-key">Used</span><span class="info-val" id="sys-disk-used">--</span></div>
<div class="info-row"><span class="info-key">Free</span><span class="info-val" id="sys-disk-free">--</span></div>
<div class="info-row"><span class="info-key">Usage</span><span class="info-val" id="sys-disk-pct">--</span></div>
</div>
</div>
</div>
</main>
</div>
<!-- Loading Overlay -->
<div class="loading-overlay" id="loading-overlay" style="display:none">
<div class="spinner"></div>
<p id="loading-text">Scanning...</p>
</div>
<script src="app.js"></script>
</body>
</html>

424
ui/style.css Normal file
View File

@@ -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; }
}