• Technitium DNS Weekly Report → ntfy / Email

    Technitium DNS Weekly Report → ntfy / Email

    #!/usr/bin/env python3
    """
    Technitium DNS Server - Weekly Device Report
    Fetches all visited & blocked sites for a target device IP
    for the last 7 days and sends via ntfy and/or email.
    
    Schedule with cron:  0 8 * * 1  python3 /path/to/dns_report.py
    """
    
    import requests
    import smtplib
    from email.mime.text import MIMEText
    from email.mime.multipart import MIMEMultipart
    from collections import Counter
    from datetime import datetime, timedelta
    import sys
    import time
    
    # ===================== CONFIGURATION =====================
    
    # Technitium DNS Server
    DNS_SERVER      = "http://192.168.1.x:5380"   # <-- Your DNS server IP
    USERNAME        = "admin"
    PASSWORD        = "your_password"              # <-- Your password
    
    # Target Device
    TARGET_DEVICE_IP = "192.168.1.100"             # <-- Device IP to report
    DEVICE_NAME      = "Test's PC"                # <-- Friendly name
    
    # Pagination (increase MAX_PAGES if you have heavy traffic)
    ENTRIES_PER_PAGE = 1000
    MAX_PAGES        = 100
    
    # ---- NTFY Configuration ----
    NTFY_ENABLED = True
    NTFY_URL     = "https://ntfy.sh/test-home"
    NTFY_TITLE   = "DNS Report"
    NTFY_PRIORITY = "default"                      # min, low, default, high, urgent
    NTFY_TAGS     = "bar_chart,globe_with_meridians"
    
    # ---- Email Configuration (optional) ----
    EMAIL_ENABLED  = False
    SMTP_SERVER    = "smtp.gmail.com"
    SMTP_PORT      = 587
    SMTP_USE_TLS   = True
    SMTP_USER      = "your_email@gmail.com"
    SMTP_PASSWORD  = "your_app_password"
    EMAIL_FROM     = "your_email@gmail.com"
    EMAIL_TO       = "recipient@gmail.com"
    EMAIL_SUBJECT  = "Weekly DNS Report"
    
    # ==========================================================
    
    
    def get_token():
        """Authenticate with Technitium and return API token."""
        url = f"{DNS_SERVER}/api/user/login"
        params = {"user": USERNAME, "pass": PASSWORD}
        try:
            resp = requests.get(url, params=params, timeout=15)
            resp.raise_for_status()
            data = resp.json()
            if data.get("status") == "ok":
                print("✅ Authenticated with Technitium DNS")
                return data["token"]
            else:
                raise Exception(f"Login failed: {data.get('errorMessage', data)}")
        except requests.exceptions.ConnectionError:
            print(f"❌ Cannot connect to Technitium at {DNS_SERVER}")
            sys.exit(1)
    
    
    def fetch_logs_for_week(token, client_ip):
        """Fetch all query logs for the target IP for the last 7 days."""
        now = datetime.now()
        week_ago = now - timedelta(days=7)
    
        # Technitium expects: yyyy-MM-dd HH:mm:ss  (or ISO 8601 in newer versions)
        start_str = week_ago.strftime("%Y-%m-%d %H:%M:%S")
        end_str   = now.strftime("%Y-%m-%d %H:%M:%S")
    
        print(f"📅 Fetching logs from {start_str} to {end_str}")
        print(f"🎯 Target device: {client_ip} ({DEVICE_NAME})\n")
    
        all_entries = []
    
        for page in range(1, MAX_PAGES + 1):
            url = f"{DNS_SERVER}/api/queryLogs/list"
            params = {
                "token":            token,
                "pageNumber":       page,
                "entriesPerPage":   ENTRIES_PER_PAGE,
                "clientIpAddress":  client_ip,
                "start":            start_str,
                "end":              end_str,
            }
    
            try:
                resp = requests.get(url, params=params, timeout=30)
                resp.raise_for_status()
                data = resp.json()
            except Exception as e:
                print(f"⚠️  Error fetching page {page}: {e}")
                break
    
            if data.get("status") != "ok":
                print(f"⚠️  API error: {data.get('errorMessage', data)}")
                break
    
            entries = data.get("response", {}).get("entries", [])
            if not entries:
                break
    
            all_entries.extend(entries)
            print(f"   Page {page}: {len(entries)} entries (total: {len(all_entries)})")
    
            # If we got fewer entries than requested, we've reached the end
            if len(entries) < ENTRIES_PER_PAGE:
                break
    
            time.sleep(0.1)  # Be gentle on the server
    
        print(f"\n📊 Total entries fetched: {len(all_entries)}")
        return all_entries
    
    
    def parse_logs(entries):
        """Parse log entries into allowed/blocked domain lists."""
        allowed_domains = []
        blocked_domains = []
        all_domains     = []
    
        for entry in entries:
            # Extract domain name (field name varies by Technitium version)
            domain = (
                entry.get("qName")
                or entry.get("questionName")
                or entry.get("qname")
                or "unknown"
            )
            domain = domain.rstrip(".")  # Remove trailing dot
    
            # Skip internal/infrastructure queries
            if domain in ("unknown", "", "localhost"):
                continue
    
            # Detect if the query was blocked
            response_type = str(entry.get("responseType", "")).lower()
            rcode         = str(entry.get("responseCode", entry.get("rcode", ""))).lower()
    
            blocked_keywords = [
                "blocked", "upstreamblocked", "cacheblocked",
                "customblocked", "blocklist", "adblocked"
            ]
    
            is_blocked = (
                any(kw in response_type for kw in blocked_keywords)
                or rcode in ("refused", "nxdomain")  # depends on your block config
                or entry.get("isBlocked", False)
            )
    
            all_domains.append(domain)
    
            if is_blocked:
                blocked_domains.append(domain)
            else:
                allowed_domains.append(domain)
    
        return allowed_domains, blocked_domains, all_domains
    
    
    def build_report(allowed_domains, blocked_domains, all_domains):
        """Build a formatted text report."""
        now       = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        week_ago  = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
        today     = datetime.now().strftime("%Y-%m-%d")
    
        allowed_counter = Counter(allowed_domains)
        blocked_counter = Counter(blocked_domains)
        all_counter     = Counter(all_domains)
    
        # ---- Build the report string ----
        lines = []
        lines.append("=" * 50)
        lines.append(f"📊 WEEKLY DNS REPORT")
        lines.append(f"📱 Device: {DEVICE_NAME} ({TARGET_DEVICE_IP})")
        lines.append(f"📅 Period: {week_ago}{today}")
        lines.append(f"🕐 Generated: {now}")
        lines.append("=" * 50)
    
        # Summary
        lines.append("")
        lines.append("📈 SUMMARY")
        lines.append(f"  Total Queries    : {len(all_domains)}")
        lines.append(f"  Allowed Queries  : {len(allowed_domains)}")
        lines.append(f"  Blocked Queries  : {len(blocked_domains)}")
        if all_domains:
            block_pct = (len(blocked_domains) / len(all_domains)) * 100
            lines.append(f"  Block Rate       : {block_pct:.1f}%")
        lines.append(f"  Unique Sites     : {len(all_counter)}")
        lines.append(f"  Unique Allowed   : {len(allowed_counter)}")
        lines.append(f"  Unique Blocked   : {len(blocked_counter)}")
    
        # ---- ALL VISITED SITES (sorted by query count) ----
        lines.append("")
        lines.append("🌐 ALL VISITED SITES (by query count)")
        lines.append("-" * 50)
    
        for i, (domain, count) in enumerate(allowed_counter.most_common(), 1):
            lines.append(f"  {i:>4}. {domain}  ({count})")
    
        # ---- ALL BLOCKED SITES ----
        lines.append("")
        lines.append("🚫 ALL BLOCKED SITES (by query count)")
        lines.append("-" * 50)
    
        if blocked_counter:
            for i, (domain, count) in enumerate(blocked_counter.most_common(), 1):
                lines.append(f"  {i:>4}. {domain}  ({count})")
        else:
            lines.append("  (No blocked queries)")
    
        lines.append("")
        lines.append("=" * 50)
    
        return "\n".join(lines)
    
    
    def build_short_summary(allowed_domains, blocked_domains, all_domains, top_n=15):
        """Build a shorter summary for ntfy (fits in message limit)."""
        allowed_counter = Counter(allowed_domains)
        blocked_counter = Counter(blocked_domains)
        week_ago = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
        today    = datetime.now().strftime("%Y-%m-%d")
    
        lines = []
        lines.append(f"📱 {DEVICE_NAME} ({TARGET_DEVICE_IP})")
        lines.append(f"📅 {week_ago}{today}")
        lines.append(f"📊 Queries: {len(all_domains)} | Blocked: {len(blocked_domains)}")
        if all_domains:
            lines.append(f"🛡️ Block rate: {(len(blocked_domains)/len(all_domains))*100:.1f}%")
        lines.append("")
    
        lines.append(f"🌐 Top {top_n} Visited:")
        for i, (domain, count) in enumerate(allowed_counter.most_common(top_n), 1):
            lines.append(f" {i}. {domain} ({count})")
    
        lines.append("")
        lines.append(f"🚫 Top {min(top_n, len(blocked_counter))} Blocked:")
        if blocked_counter:
            for i, (domain, count) in enumerate(blocked_counter.most_common(top_n), 1):
                lines.append(f" {i}. {domain} ({count})")
        else:
            lines.append(" (none)")
    
        return "\n".join(lines)
    
    
    def send_ntfy(full_report, short_summary):
        """Send notification via ntfy.sh with full report as attachment."""
        if not NTFY_ENABLED:
            return
    
        print("\n📤 Sending ntfy notification...")
    
        # ---- Send the full report as a file attachment ----
        try:
            timestamp = datetime.now().strftime("%Y%m%d")
            filename = f"dns_report_{timestamp}.txt"
    
            resp = requests.put(
                NTFY_URL,
                data=full_report.encode("utf-8"),
                headers={
                    "Title":    NTFY_TITLE,
                    "Tags":     NTFY_TAGS,
                    "Priority": NTFY_PRIORITY,
                    "Filename": filename,
                },
                timeout=15,
            )
            resp.raise_for_status()
            print(f"   ✅ Full report sent as attachment: {filename}")
        except Exception as e:
            print(f"   ⚠️  Attachment send failed: {e}")
    
        # ---- Send a short summary as a readable notification ----
        try:
            resp = requests.post(
                NTFY_URL,
                data=short_summary.encode("utf-8"),
                headers={
                    "Title":    f"{NTFY_TITLE} - {DEVICE_NAME}",
                    "Tags":     NTFY_TAGS,
                    "Priority": NTFY_PRIORITY,
                },
                timeout=15,
            )
            resp.raise_for_status()
            print("   ✅ Summary notification sent!")
        except Exception as e:
            print(f"   ❌ Notification failed: {e}")
    
    
    def send_email(full_report):
        """Send the full report via email."""
        if not EMAIL_ENABLED:
            return
    
        print("\n📧 Sending email report...")
    
        try:
            msg = MIMEMultipart("alternative")
            msg["Subject"] = f"{EMAIL_SUBJECT} - {DEVICE_NAME}"
            msg["From"]    = EMAIL_FROM
            msg["To"]      = EMAIL_TO
    
            # Plain text version
            text_part = MIMEText(full_report, "plain", "utf-8")
            msg.attach(text_part)
    
            # HTML version (optional - makes it look nicer)
            html_report = "<pre style='font-family: monospace; font-size: 13px;'>\n"
            html_report += full_report.replace("\n", "<br>")
            html_report += "\n</pre>"
            html_part = MIMEText(html_report, "html", "utf-8")
            msg.attach(html_part)
    
            with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
                if SMTP_USE_TLS:
                    server.starttls()
                server.login(SMTP_USER, SMTP_PASSWORD)
                server.sendmail(EMAIL_FROM, EMAIL_TO, msg.as_string())
    
            print("   ✅ Email sent successfully!")
        except Exception as e:
            print(f"   ❌ Email failed: {e}")
    
    
    def save_report_local(full_report):
        """Save the report to a local file as backup."""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"dns_report_{TARGET_DEVICE_IP}_{timestamp}.txt"
        try:
            with open(filename, "w", encoding="utf-8") as f:
                f.write(full_report)
            print(f"\n💾 Report saved locally: {filename}")
        except Exception as e:
            print(f"\n⚠️  Could not save locally: {e}")
    
    
    # ===================== MAIN =====================
    
    def main():
        print("🚀 Technitium DNS Weekly Report Generator\n")
    
        # Step 1: Authenticate
        token = get_token()
    
        # Step 2: Fetch logs for the last 7 days
        entries = fetch_logs_for_week(token, TARGET_DEVICE_IP)
    
        if not entries:
            no_data_msg = f"No DNS queries found for {DEVICE_NAME} ({TARGET_DEVICE_IP}) in the last 7 days."
            print(f"\n⚠️  {no_data_msg}")
    
            # Still notify that there's no data
            if NTFY_ENABLED:
                requests.post(
                    NTFY_URL,
                    data=no_data_msg.encode("utf-8"),
                    headers={"Title": NTFY_TITLE, "Tags": "warning"},
                    timeout=15,
                )
            return
    
        # Step 3: Parse logs
        allowed, blocked, all_domains = parse_logs(entries)
        print(f"\n   Allowed: {len(allowed)} | Blocked: {len(blocked)} | Total: {len(all_domains)}")
    
        # Step 4: Build reports
        full_report   = build_report(allowed, blocked, all_domains)
        short_summary = build_short_summary(allowed, blocked, all_domains, top_n=15)
    
        # Print to console
        print("\n" + full_report)
    
        # Step 5: Send notifications
        send_ntfy(full_report, short_summary)
        send_email(full_report)
        save_report_local(full_report)
    
        print("\n🎉 Done!")
    
    
    if __name__ == "__main__":
        main()
    

    What This Does

    ┌─────────────────────────────────────────────┐
    │         Technitium DNS Server               │
    │         (last 7 days of logs)               │
    └──────────────┬──────────────────────────────┘
                   │ API query filtered by IP
                   ▼
    ┌─────────────────────────────────────────────┐
    │         Python Script                       │
    │  • Fetches all logs for TARGET_DEVICE_IP    │
    │  • Separates allowed vs blocked             │
    │  • Counts & ranks domains                   │
    │  • Builds full report + short summary       │
    └──────┬──────────────┬──────────────┬────────┘
           │              │              │
           ▼              ▼              ▼
       📱 ntfy         📧 Email      💾 Local
       (2 messages)    (full report)   (.txt file)
       • Summary msg
       • Full .txt
         attachment
    

    ntfy Notifications You’ll Receive

    Notification 1 — Readable Summary:

    📱 Test's PC (192.168.1.100)
    📅 2025-01-08 → 2025-01-15
    📊 Queries: 12847 | Blocked: 2341
    🛡️ Block rate: 18.2%
    
    🌐 Top 15 Visited:
     1. www.google.com (834)
     2. youtube.com (612)
     3. reddit.com (445)
     ...
    
    🚫 Top 15 Blocked:
     1. ads.doubleclick.net (312)
     2. tracking.facebook.com (198)
     ...
    

    Notification 2 — Full report attached as dns_report_20250115.txt


    Setup Steps

    1. Edit the Configuration

    DNS_SERVER       = "http://192.168.1.5:5380"    # Your Technitium IP
    PASSWORD         = "your_actual_password"
    TARGET_DEVICE_IP = "192.168.1.100"              # Device to track
    DEVICE_NAME      = "Test's PC"
    

    2. Enable Query Logging in Technitium

    Technitium DashboardSettingsLogging

    • ✅ Enable Query Logging
    • ✅ Log to local folder
    • Set log retention ≥ 7 days

    3. Test It

    pip install requests
    python3 dns_report.py
    

    4. Schedule Weekly (Every Monday 8 AM)

    Linux (cron):

    crontab -e
    # Add:
    0 8 * * 1 /usr/bin/python3 /home/test/dns_report.py >> /home/test/dns_report.log 2>&1
    

    Windows (Task Scheduler):

    Action: Start a program
    Program: python
    Arguments: C:\scripts\dns_report.py
    Trigger: Weekly, Monday, 8:00 AM
    

    Optional: Enable Email Too

    Set EMAIL_ENABLED = True and fill in your SMTP details. For Gmail, use an App Password:

    EMAIL_ENABLED  = True
    SMTP_SERVER    = "smtp.gmail.com"
    SMTP_PORT      = 587
    SMTP_USER      = "you@gmail.com"
    SMTP_PASSWORD  = "xxxx xxxx xxxx xxxx"  # Gmail App Password
    EMAIL_TO       = "you@gmail.com"
    
  • PowerToys Peek on Spacebar Is a UX Trap in Windows 11

    Windows 11 Spacebar Opens File Preview? It’s Probably PowerToys — And It’s Bad UX

    If pressing Spacebar in Windows 11 File Explorer opens a preview window, it’s easy to assume Windows is broken.

    It probably isn’t.

    In most cases, the real culprit is PowerToys Peek — and if it’s bound to Spacebar, it creates a genuinely bad user experience.

    Why? Because Spacebar is not a “feature key.” It’s a typing key.

    That means a preview shortcut can suddenly hijack normal actions like:

    • renaming files,
    • typing in search,
    • or simply using File Explorer the way people expect it to work.

    This post explains what’s happening, how to fix it, and why binding Peek to Spacebar is a design choice that can easily backfire.


    TL;DR

    If Spacebar previews files in Windows 11, the issue is usually:

    • PowerToys → Peek
    • configured to use Spacebar

    Fix:

    Open:

    PowerToys → Peek

    Then either:

    • Disable Peek, or
    • Change the activation shortcut to something like:
      • Ctrl + Space
      • Ctrl + Shift + Space

    First: This Is Not a Native Windows 11 Feature

    This part matters.

    Windows 11 does not natively use plain Spacebar in File Explorer to open a floating preview popup.

    What Windows does include is:

    • Preview Pane
    • Shortcut: Alt + P
    • Opens as a side panel inside File Explorer

    That’s the built-in behavior.

    If you’re getting a floating popup preview when pressing Spacebar, that’s almost always coming from:

    • PowerToys Peek
    • QuickLook
    • Seer
    • or another third-party Explorer enhancement

    In my case, it was PowerToys Peek.


    Why This Feels Like a Windows Bug

    Because PowerToys is made by Microsoft.

    That’s what makes this so misleading.

    PowerToys integrates well with Windows, so when it changes File Explorer behavior, it can feel “native” enough that users assume the OS itself is responsible.

    But PowerToys is not Windows.

    It’s an optional utility suite layered on top of Windows — and that distinction matters when troubleshooting weird keyboard behavior.


    The Real Problem Isn’t the Preview — It’s the Shortcut

    The preview feature itself is not the issue.

    The issue is binding it to Spacebar.

    That’s where the UX falls apart.

    Why this is bad UX

    Spacebar is a core input key.

    It’s not like F8, Ctrl + Shift + P, or some obscure combo nobody types accidentally.

    Users press Spacebar all the time, and not just in documents.

    Inside File Explorer alone, it’s part of normal workflow behavior.

    That means assigning a global-ish preview action to Spacebar creates a shortcut collision with basic file operations.


    The Best Example: Renaming Files

    This is where the problem becomes obvious.

    Typical workflow

    1. Select a file
    2. Press F2 to rename it
    3. Start typing the new file name
    4. Press Spacebar to separate words

    What happens instead

    Instead of inserting a space in the file name:

    • Peek intercepts the keypress
    • a preview window opens
    • your rename flow gets interrupted

    That’s not just annoying.

    That’s a direct conflict with one of the most common actions in File Explorer.

    Why this matters

    When a utility hijacks a normal text-input key, users experience:

    • broken typing behavior
    • interrupted workflows
    • accidental popups
    • confusion about what the system is doing
    • loss of trust in File Explorer behavior

    That’s why this feels worse than “just a shortcut issue.”

    It’s a usability problem.


    How I Confirmed It Was PowerToys

    The fastest test took about 10 seconds.

    Quick diagnostic

    1. Press Ctrl + Shift + Esc to open Task Manager
    2. Go to Processes
    3. Find PowerToys
    4. Right-click it
    5. Click End task
    6. Go back to File Explorer
    7. Press Spacebar on a selected file

    Result

    If the preview stops:

    PowerToys Peek was the cause

    That immediately proves the behavior is not coming from native Windows.


    How to Fix It

    You have two options:

    • disable Peek
    • or keep Peek, but stop using Spacebar

    For most people, the second option is the better fix.


    Option 1: Disable Peek Completely

    If you don’t use file preview often:

    1. Open PowerToys
    2. Click Peek in the left sidebar
    3. Turn Enable Peek Off

    Done.


    If you like the feature, keep it — just don’t bind it to a typing key.

    Steps

    1. Open PowerToys
    2. Go to Peek
    3. Find Activation shortcut
    4. Change it from Spacebar to something safer

    Better shortcut choices

    • Ctrl + Space
    • Ctrl + Shift + Space
    • another modifier-based combination that doesn’t conflict with typing

    This preserves the feature without breaking normal file operations.


    My Opinion: Spacebar Should Never Be the Default Here

    This is the part where I’ll be blunt:

    Spacebar is a poor default for a preview tool inside File Explorer.

    Yes, it mirrors the macOS Quick Look idea.

    But Windows users don’t all have the same expectations or workflows, and File Explorer is not Finder.

    More importantly:

    • Spacebar is still a normal input key
    • it’s heavily used in naming, searching, and general text entry
    • binding it to a preview action creates too much friction unless it’s extremely context-aware

    A good shortcut should be:

    • intentional,
    • hard to trigger accidentally,
    • and should not interfere with typing

    Spacebar fails that test in this context.


    If you use PowerToys Peek, avoid assigning it to:

    • Spacebar
    • single letters
    • number keys
    • any key that doubles as regular text input

    Use modifier-based shortcuts instead

    Safer examples:

    • Ctrl + Space
    • Ctrl + Shift + Space
    • Alt + Space (only if it doesn’t conflict with your setup)

    The rule is simple:

    If a key is part of normal typing, it shouldn’t be the sole trigger for a utility action in File Explorer.


    If It’s Not PowerToys

    If disabling or rebinding Peek doesn’t fix it, check for similar tools:

    • QuickLook
    • Seer
    • Files
    • QTTabBar

    But if you already found PowerToys Peek bound to Spacebar, that’s almost certainly the root cause.


    Final Take

    The weird part about this issue is that it looks like Windows.

    The frustrating part is that it feels like a Windows bug.

    But the real problem is simpler:

    • PowerToys Peek was bound to Spacebar
    • Spacebar is a terrible key to hijack for this
    • That breaks common workflows like file renaming

    Best fix

    Open:

    PowerToys → Peek

    Then:

    • disable Peek, or
    • rebind it to a modifier-based shortcut

    That keeps the feature useful without making File Explorer feel broken.


  • Lenovo Workstation Recommendation for Research

    [2] ASSISTANT (claude-opus-4-6-thinking)

    💭 Thinking Process > The user wants to buy a Lenovo workstation for a university researcher who will be using: > 1. **Abaqus** (FEA simulation software) > 2. **MATLAB** (numerical computing) > 3. **CATIA** (3D CAD/CAM/CAE software) > > Let me outline the system requirements for each and then recommend a configuration that meets or exceeds all of them. > > ## Abaqus (Dassault Systèmes) > **Minimum Requirements:** > - CPU: 64-bit Intel or AMD processor (multi-core recommended) > - RAM: 8 GB minimum, 16-64 GB+ recommended for large models > > > - GPU: OpenGL compatible, certified graphics cards (NVIDIA Quadro/RTX or AMD Radeon Pro) > - Storage: SSD recommended, 10+ GB for installation > - OS: Windows 10/11 64-bit or Linux > > **Recommended for serious FEA work:** > - CPU: Multi-core (8+ cores), high clock speed > - RAM: 64-128 GB for large simulations > - GPU: NVIDIA RTX A-series or Quadro > - Storage: NVMe SSD (1 TB+) > > ## MATLAB (MathWorks) > Now I'm looking at MATLAB's requirements, which are fairly flexible—it runs on standard Intel or AMD processors with modest RAM needs, though for heavy numerical work I'd want at least 16 GB, and the software itself needs about 2-3 GB of disk space. > > ## CATIA (Dassault Systèmes) > CATIA is more demanding, requiring high-performance Intel Core i7 or Xeon processors and a minimum of 16 GB RAM, with certified NVIDIA or AMD GPUs being essential for proper performance. > > ## Combined Recommendation > > For serious simulation work at a university, I'm thinking a workstation-class machine makes the most sense. The Lenovo ThinkStation line offers several solid options depending on budget—the P3 Tower for mid-range needs, the P620 with AMD Threadripper Pro for heavy multithreading, or the P520 with Intel Xeon W for maximum single-thread performance. > > Looking at the performance tiers, I'd recommend either a 16+ core processor with 64-128 GB of ECC RAM and an RTX A4000 or A5000 GPU for the serious simulation work, or step down to an 8-core Xeon W with 32 GB RAM and an RTX A2000 if budget is tighter. Storage-wise, a 1 TB NVMe SSD for the OS and applications, plus a 2 TB HDD for data, gives good balance between speed and capacity. For a researcher, the P3 Tower or P520 would work well, though I should consider the current 2024-2025 lineup more carefully—the P5 has replaced the P520, and there's also the high-end PX option with dual Xeons if they need maximum performance. Let me put together a practical recommendation based on their actual needs.

    Lenovo Workstation Recommendation for Research (Abaqus, MATLAB, CATIA)

    Component Abaqus MATLAB CATIA You Should Target
    CPU Multi-core 64-bit; high clock + many cores for solvers x86-64 with AVX2 Intel Xeon / Core i7+ Xeon W or Core i9 (12–16+ cores)
    RAM 8 GB min; 64 GB+ for large models 4 GB min 16 GB min 64 GB ECC (expandable to 128 GB)
    GPU OpenGL; certified NVIDIA Quadro/RTX NVIDIA CUDA for GPU computing Certified NVIDIA RTX / Quadro (critical for CATIA) NVIDIA RTX A4000 (16 GB) or RTX A2000 (12 GB)
    Storage SSD strongly recommended; large scratch space ~10–30 GB with toolboxes SSD required 1 TB NVMe SSD + optional 2 TB HDD
    OS Windows 10/11 Pro 64-bit or Linux Windows/Linux Windows 10/11 Pro 64-bit Windows 11 Pro 64-bit

    ⚠️ CATIA is the most demanding regarding GPU certification. Always cross-check the chosen GPU against the Dassault Systèmes certified hardware list.


    ✅ Option 1: Lenovo ThinkStation P3 Tower (Best value for most researchers)

    Component Suggested Config
    Processor Intel Core i9-13900K (24 cores/32 threads) or Intel Xeon W3-2435 (8 cores, ECC support)
    RAM 64 GB DDR5 (2 × 32 GB), expandable to 128 GB
    GPU NVIDIA RTX A4000 (16 GB GDDR6)
    Storage 1 TB PCIe Gen4 NVMe SSD + 2 TB 7200 RPM HDD
    PSU 750W (80+ Platinum)
    OS Windows 11 Pro 64-bit
    Approx. Price $3,500–$5,000 USD

    🔶 Option 2: Lenovo ThinkStation P620 (Heavy simulation workloads)

    Component Suggested Config
    Processor AMD Threadripper PRO 5955WX (16 cores/32 threads) or 5975WX (32 cores)
    RAM 128 GB DDR4 ECC (4 × 32 GB), expandable to 512 GB
    GPU NVIDIA RTX A5000 (24 GB GDDR6)
    Storage 1 TB PCIe Gen4 NVMe SSD + 4 TB HDD
    PSU 1000W
    OS Windows 11 Pro 64-bit
    Approx. Price $6,000–$10,000 USD

    Key Points & Tips

    1. Abaqus scaling: Abaqus/Standard benefits from high clock speed; Abaqus/Explicit benefits from more cores. Ask the researcher what type of analyses they run.

    2. ECC Memory: For long-running simulations (hours/days), ECC RAM is strongly recommended to prevent silent data corruption. Xeon/Threadripper Pro platforms support this.

    3. GPU Certification: CATIA can be unstable on non-certified GPUs. Stick with NVIDIA RTX A-series (A2000, A4000, A5000) — these are on Dassault’s certified list.

    4. MATLAB Parallel Computing: If the researcher uses MATLAB’s Parallel Computing Toolbox or GPU computing, ensure the NVIDIA GPU has CUDA Compute Capability 3.5+ (all RTX A-series qualify).

    5. Scratch/Temp Storage: Abaqus writes large temporary files during solves. A secondary NVMe SSD dedicated to scratch space can significantly speed up large simulations.

  • White Desktop Icon in Windows 11 Caused by OneDrive

    How To Fix the Zombie White Desktop Icon in Windows 11 Caused by OneDrive

    If you’ve deployed a customized Windows 11 image and are seeing a mysterious blank white desktop icon that reappears every time you delete it — even though it has no name, target, or properties — you’re not alone.

    This ghostly icon is one of the most frustrating issues in Windows imaging and deployment. But the good news? It’s not random, and it can be permanently fixed.

    In this guide, I’ll show you exactly how to identify, remove, and prevent this issue — especially when it’s caused by OneDrive, which is one of the most common root causes.


    🧟‍♂️ What Is This “Zombie” Icon?

    You see:

    • A single blank white icon on the desktop
    • No right-click menu options (or only “Delete”)
    • Properties shows nothing useful (no target path)
    • Deleting it → refresh (F5) → it comes back instantly
    • Appears for every new user on machines deployed from your image

    ❌ This is NOT an icon cache issue
    ✅ This is a namespace object baked into the Default User profile


    🔍 Why Does This Happen?

    When you run sysprep /generalize /oobe /unattend:unattend.xml with CopyProfile enabled, Windows copies settings from your reference user account to the Default User profile.

    If OneDrive was ever installed, partially removed, disabled, or unprovisioned during your build process, its desktop namespace registration may have been left behind as an orphaned registry entry.

    Even if you:

    • Removed OneDrive via PowerShell
    • Disabled it via Group Policy
    • Deleted the shortcut manually

    …if the namespace key remains in the registry, it gets copied to all future users.

    And because the app can’t be found at login, Explorer displays it as a white placeholder icon.


    ✅ Step-by-Step Fix: Remove the Orphaned OneDrive Namespace Entry

    🔧 For Already-Deployed Machines (Quick Fix)

    1. Press Win + R, type regedit, and press Enter
    2. Navigate to:
      HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace
      
    3. Look through each subkey (they are GUIDs like {A0953C92-50DC-43BF-BE83-575D374FBC1}).
    4. Click each one and check the (Default) value in the right pane.
    5. Find the key where the Default value says “OneDrive”.
    6. Right-click that GUID key → Export (to backup) → then Delete
    7. Also check the same path under:
      HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace
      

      And delete the OneDrive GUID there too, if present.

    8. Restart Explorer or log off and back in:
      • Open Task Manager → find Windows Explorer → click Restart

    ✅ The white icon should now be gone — and stay gone.


    💣 Nuclear Option: Fix It Permanently in Your Reference Image

    To ensure this never happens again on any machine you deploy, clean up the Default User profile before capturing your image.

    ⚠️ Warning:

    Never edit C:\Users\Default directly. Always use the registry hive method.


    Step 1: Load the Default User Registry Hive

    Run Command Prompt as Administrator on your reference machine:

    reg load HKLM\DefaultUser C:\Users\Default\NTUSER.DAT
    

    This loads the Default User’s NTUSER.DAT into the registry under HKEY_LOCAL_MACHINE\DefaultUser.


    Step 2: Delete the OneDrive Namespace Key

    1. Open regedit
    2. Go to:
      HKEY_LOCAL_MACHINE\DefaultUser\Software\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace
      
    3. Browse each GUID subkey.
    4. If you find a key where the (Default) value is “OneDrive”, delete it.
      • Example GUIDs (may vary):
        • {018D5C66-4533-4307-9B53-224DE2ED1FE6} ← Common OneDrive namespace
        • {905E63B6-C1BF-494E-B29C-65B732D3D21A} ← Older Microsoft 365/OneDrive combo

    🔎 Tip: Sort by the “Data” column to spot “OneDrive” entries faster.


    Step 3: Unload the Hive (Critical!)

    Back in the admin Command Prompt:

    reg unload HKLM\DefaultUser
    

    ❗ Never skip this step. Failing to unload the hive can corrupt the Default profile and cause bigger issues.


    Step 4: Run Sysprep & Capture

    Now proceed with:

    sysprep /generalize /oobe /shutdown /unattend:unattend.xml
    

    Then capture your WIM/ESD image.

    🎉 Any machine deployed from this image will no longer inherit the OneDrive zombie icon.


    🛠 Prevent This Issue in Future Builds

    Add these steps to your standard image build checklist:

    Step Action
    1 After removing apps (like OneDrive), always clean the namespace registry
    2 Use reg load / reg unload to audit the Default User profile
    3 Consider disabling OneDrive via GPO instead of removal, unless required
    4 Avoid interactive logins before sysprep — they risk polluting the profile

    📜 Bonus: PowerShell Script to Remove OneDrive Namespace (For Deployment Tools)

    Use this in MDT, SCCM, or Intune to fix existing machines:

    # Remove OneDrive from Machine-wide Desktop Namespace
    $oneDriveGuids = '{018D5C66-4533-4307-9B53-224DE2ED1FE6}', '{905E63B6-C1BF-494E-B29C-65B732D3D21A}'
    
    foreach ($guid in $oneDriveGuids) {
        $path = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\$guid"
        if (Test-Path $path) {
            Remove-Item -Path $path -Force -ErrorAction SilentlyContinue
        }
    }
    
    # Optional: Kill explorer and restart to refresh immediately
    Get-Process explorer | Stop-Process
    

    Add this script to your task sequence just before sysprep.


    🔄 Alternative: Disable OneDrive Without Breaking the Desktop

    Instead of removing OneDrive entirely, consider suppressing it more cleanly:

    • Path: User Configuration → Administrative Templates → Windows Components → OneDrive
    • Enable: “Remove OneDrive shortcut from This PC”

    This hides the icon without leaving broken registry entries.


    ✅ Final Thoughts

    That blank white desktop icon is not magic — it’s a misconfigured namespace object inherited from a dirty Default User profile.

    By understanding that OneDrive is a frequent offender, and using the reg load / reg unload method to clean the Default hive, you can:

    • Fix existing deployments
    • Prevent future ones
    • Save hours of troubleshooting across your fleet

  • Building an OEM-Style Factory Reset for Windows 11 — Complete Guide

    Building an OEM-Style Factory Reset for Windows 11 — Complete Guide

    A Step-by-Step Blog Post


    # How to Create an OEM-Style Factory Reset Partition in Windows 11
    ### Restore Your PC with All Apps, Drivers & Settings — Like Dell/HP/Lenovo
    
    ---
    
    ## Table of Contents
    
    1. [Introduction](#introduction)
    2. [Architecture Overview](#architecture-overview)
    3. [Prerequisites](#prerequisites)
    4. [Phase 1: Create the Recovery Partition](#phase-1-create-the-recovery-partition)
    5. [Phase 2: The PowerShell Script — Explained](#phase-2-the-powershell-script)
    6. [Phase 3: How the Restore Scripts Work](#phase-3-restore-scripts)
    7. [Phase 4: Testing & Validation](#phase-4-testing)
    8. [Lessons Learned & Pitfalls](#lessons-learned)
    9. [Complete Script Download](#complete-script)
    10. [FAQ](#faq)
    
    ---
    
    ## Introduction
    
    Ever wondered how Dell, HP, and Lenovo ship PCs with a "Factory Reset" option 
    that restores Windows with **all pre-installed applications and drivers**? 
    
    Windows 11's built-in "Reset this PC" feature **strips away all Win32 
    desktop applications** during the reset process. It's designed to return 
    Windows to a clean state — not to restore a fully configured system.
    
    This guide shows you how to build a **true OEM-style factory reset** 
    that preserves everything:
    
    - ✅ All installed applications (Win32, .NET, etc.)
    - ✅ All hardware drivers  
    - ✅ System settings and configurations
    - ✅ Silent OOBE (no setup screens after restore)
    - ✅ Hidden, protected recovery partition
    - ✅ Works even when Windows won't boot
    
    ### Why Not Just Use "Reset this PC"?
    
    | Feature | Reset this PC | Our Solution |
    |---------|:------------:|:------------:|
    | Restores base Windows | ✅ | ✅ |
    | Keeps Win32 apps | ❌ | ✅ |
    | Keeps drivers | ❌ | ✅ |
    | Silent OOBE | ❌ | ✅ |
    | Works from hidden partition | ❌ | ✅ |
    | OEM-style experience | ❌ | ✅ |
    
    ---
    
    ## Architecture Overview
    
    ### How It Works
    
    ```text
    CREATING THE RECOVERY IMAGE:
    
    ┌─────────────────┐     VSS Snapshot     ┌──────────────────┐
    │  C:\ (live OS)  │ ──────────────────→  │ Shadow Copy      │
    │  Files locked   │                      │ No locks ✅      │
    └─────────────────┘                      └────────┬─────────┘
                                                      │
                                               DISM /Capture
                                                      │
                                             ┌────────▼─────────┐
                                             │ Recovery Partition│
                                             │ install.wim      │
                                             │ unattend.xml     │
                                             │ restore.cmd      │
                                             └──────────────────┘
    
    RESTORING THE SYSTEM:
    
    ┌─────────────────┐
    │ Boot to WinRE   │
    │ (SHIFT+Restart) │
    └────────┬────────┘
             │
             ▼
    ┌───────────────────────────────┐
    │ CMD Prompt                    |
    |   Diskpart                    │
    | list vol                      |
    |check for the vol name recovery|
    |Select that volume             |
    |Assign Letter R                |
    |CD R:\RecoveryImage            |
    │ Run restore.cmd               │
    └────────┬──────────────────────┘
             │
             ▼
    ┌─────────────────────────────────────────┐
    │ restore.cmd:                             │
    │ 1. Scan all drives for install.wim       │
    │ 2. Format C:                             │
    │ 3. DISM /Apply-Image to C:\              │
    │ 4. Rebuild bootloader                    │
    │ 5. Apply unattend.xml                    │
    │ 6. Re-hide recovery partition            │
    │ 7. Reboot → Windows ready!               │
    └──────────────────────────────────────────┘
    

    File Structure

    Recovery Partition (hidden):
    └── RecoveryImage/
        ├── install.wim           ← Full system image (15-40 GB)
        ├── unattend.xml          ← Silent OOBE configuration
        ├── restore.cmd           ← Interactive factory reset
        ├── diagnose.cmd          ← Safe pre-restore testing
        ├── quick_restore.cmd     ← Automated (no prompts)
        ├── recovery_metadata.txt ← Partition info reference
        └── INSTRUCTIONS.txt      ← Human-readable guide
    
    Desktop (on C:):
    ├── Factory Reset.bat                 ← Double-click launcher
    ├── FactoryReset_Launcher.ps1        ← PowerShell confirmation
    └── Factory Reset - Instructions.txt ← Restore guide
    

    Prerequisites

    System Requirements

    • Windows 11 (any edition)
    • Administrator privileges
    • All desired applications and drivers installed
    • At least 30-50 GB free space for recovery partition
    • UEFI boot mode (not Legacy BIOS)

    Before You Begin

    1. Install all applications and drivers
    2. Configure all settings
    3. Run Windows Update
    4. Clean up temp files (Disk Cleanup)
    5. Restart the PC for a clean state

    Phase 1: Create the Recovery Partition

    Before running the script, create a partition manually:

    Step 1: Open Disk Management

    Win + X → Disk Management
    

    Step 2: Shrink C: Drive

    1. Right-click C: → "Shrink Volume"
    2. Enter amount: 30000-50000 MB (30-50 GB)
       (Should be ~70% of your used space on C:)
    3. Click "Shrink"
    

    Step 3: Create New Volume

    1. Right-click the new "Unallocated" space
    2. "New Simple Volume"
    3. Wizard:
       - Use all available space
       - Assign letter: R
       - Format: NTFS
       - Volume Label: Recovery
    4. Click "Finish"
    

    You should now see a Recovery (R:) drive in File Explorer.


    Phase 2: The PowerShell Script

    How the Script Works

    The PowerShell script automates 4 steps:

    Step What It Does How
    1 Capture system image VSS Shadow Copy + DISM /Capture
    1.5 Generate silent OOBE config Creates unattend.xml
    2 Create restore scripts Generates WinRE-compatible .cmd files
    3 Create desktop shortcuts Factory Reset.bat + instructions
    4 Hide partition (optional) DiskPart removes letter + sets GPT attributes

    Key Technical Decisions

    Why VSS Shadow Copy?

    You cannot capture a live Windows drive — files are locked by the OS:

    Direct DISM on C:\ → ERROR 0x80070020 (Sharing Violation)
    

    VSS creates a frozen point-in-time snapshot that DISM can capture without any file locks.

    # Create shadow copy
    $result = (Get-WmiObject -List Win32_ShadowCopy).Create("C:\", "ClientAccessible")
    
    # Mount it
    cmd /c "mklink /d C:\ShadowCopyMount \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy1\"
    
    # Capture from snapshot (no locks!)
    DISM.exe /Capture-Image /ImageFile:R:\RecoveryImage\install.wim `
             /CaptureDir:C:\ShadowCopyMount /Name:"Recovery" /Compress:maximum
    

    Why Not “Reset this PC” with reagentc?

    reagentc /setosimage → "Reset this PC" → Windows RECONSTRUCTS the OS
                                            → Win32 apps are STRIPPED OUT ❌
    
    DISM /Apply-Image → Writes image BYTE-FOR-BYTE back to disk
                      → EVERYTHING preserved ✅
    

    Why Hardcoded Paths in restore.cmd?

    WinRE has a stripped-down cmd.exe that doesn’t support EnableDelayedExpansion:

    :: BROKEN in WinRE:
    setlocal EnableDelayedExpansion
    set WIM=R:\RecoveryImage\install.wim
    DISM.exe /ImageFile:"!WIM!"Expands to ""Error 87
    
    :: WORKS in WinRE:
    DISM.exe /ImageFile:R:\RecoveryImage\install.wim    ← Hardcoded, always works
    

    Why Scan All Drive Letters?

    WinRE reassigns drive letters differently than normal Windows:

    Normal Windows:     WinRE:
      C: = Windows        C: = Windows (usually)
      R: = Recovery        D: = Recovery (auto-assigned!)
                           R: = nothing
    

    The restore script scans every letter before resorting to diskpart:

    if exist D:\RecoveryImage\install.wim set RECOVERY_DRIVE=D:& goto FOUND
    if exist E:\RecoveryImage\install.wim set RECOVERY_DRIVE=E:& goto FOUND
    ...
    :: If none found, mount via diskpart with known disk/partition numbers
    

    Why ping Instead of timeout?

    timeout.exe doesn’t exist in WinRE:

    timeout /t 3 /nobreak > nulERROR in WinRE
    ping -n 4 127.0.0.1 > nulWorks everywhere (3 second delay)
    

    Why Separate DiskPart Calls for Re-hide?

    Combining all operations in one script causes cascading failures:

    :: BROKEN: If "remove letter" fails, set id and gpt also fail
    select partition 5
    remove letter=Rfails if letter isn't R
    set id=de94bba4...       ← skipped!
    gpt attributes=0x80...   ← skipped!
    
    :: FIXED: Each operation is independent
    :: Call 1:
    select partition 5
    remove                   ← removes whatever letter is assigned
    exit
    
    :: Call 2:
    select partition 5
    set id=de94bba4-06d1-4d40-a16a-bfd50179d6ac override
    exit
    
    :: Call 3:
    select partition 5
    gpt attributes=0x8000000000000001
    exit
    

    The Complete Script

    Save as Create-RecoveryPartition.ps1 and run as Administrator:

    #Requires -RunAsAdministrator
    <#
    .SYNOPSIS
        Windows 11 Recovery Partition Creator — Final Version
        
    .DESCRIPTION
        All fixes included:
        - VSS Shadow Copy for WIM capture
        - Hardcoded DISM paths (no variables, no delayed expansion)
        - ping instead of timeout (WinRE compatible)
        - Separate diskpart calls for re-hide
        - Full logging to C:\temp\restore_log.txt
        - Unattend.xml for silent OOBE
        - Hidden partition support
        - Desktop shortcuts
    
    .NOTES
        Run as Administrator
        Requires existing recovery partition (create in Disk Management first)
    #>
    
    # ============================================================
    # CONFIGURATION
    # ============================================================
    $RecoveryDriveLetter = ""
    $RecoveryFolderName  = "RecoveryImage"
    $RecoveryVolumeLabel = "Recovery"
    $ImageName           = "Windows11CustomRecovery"
    $ImageDescription    = "FullSystemWithAppsAndDrivers"
    $CompressionType     = "maximum"
    $LogFile             = "$env:USERPROFILE\Desktop\RecoverySetup_Log.txt"
    $ShadowMountPoint    = "C:\ShadowCopyMount"
    
    # ============================================================
    # LOGGING
    # ============================================================
    
    function Write-Log {
        param([string]$Message, [string]$Level = "INFO")
        $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
        $logEntry  = "[$timestamp] [$Level] $Message"
        switch ($Level) {
            "INFO"    { Write-Host $logEntry -ForegroundColor Cyan }
            "SUCCESS" { Write-Host $logEntry -ForegroundColor Green }
            "WARNING" { Write-Host $logEntry -ForegroundColor Yellow }
            "ERROR"   { Write-Host $logEntry -ForegroundColor Red }
            "STEP"    {
                Write-Host ""
                Write-Host ("=" * 70) -ForegroundColor Magenta
                Write-Host "  $Message" -ForegroundColor Magenta
                Write-Host ("=" * 70) -ForegroundColor Magenta
            }
        }
        Add-Content -Path $LogFile -Value $logEntry
    }
    
    function Show-Banner {
        Clear-Host
        Write-Host ""
        Write-Host "  ╔════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
        Write-Host "  ║  Windows 11 - Recovery Partition Creator (Final)           ║" -ForegroundColor Cyan
        Write-Host "  ║  Full System Restore — Apps + Drivers + Settings           ║" -ForegroundColor Cyan
        Write-Host "  ║  WinRE Compatible — Logging — Hidden Partition Support    ║" -ForegroundColor Cyan
        Write-Host "  ╚════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
        Write-Host ""
    }
    
    # ============================================================
    # PARTITION SELECTION & PREREQUISITES
    # ============================================================
    
    function Show-AvailablePartitions {
        $volumes = Get-Volume | Where-Object {
            $_.DriveLetter -and $_.DriveType -eq 'Fixed' -and $_.DriveLetter -ne 'C'
        } | Sort-Object DriveLetter
    
        if ($volumes.Count -eq 0) {
            Write-Log "No suitable partitions found!" "ERROR"
            Write-Host ""
            Write-Host "  Create a recovery partition first:" -ForegroundColor Yellow
            Write-Host "  1. Win+X → Disk Management" -ForegroundColor White
            Write-Host "  2. Shrink C: by 30-50 GB" -ForegroundColor White
            Write-Host "  3. Create New Simple Volume (NTFS, label: Recovery)" -ForegroundColor White
            Write-Host ""
            return $null
        }
    
        Write-Host "  Available Drives:" -ForegroundColor Yellow
        Write-Host "  ────────────────────────────────────────────────────────" -ForegroundColor Gray
        foreach ($vol in $volumes) {
            $sizeGB = [math]::Round($vol.Size / 1GB, 2)
            $freeGB = [math]::Round($vol.SizeRemaining / 1GB, 2)
            $label  = if ($vol.FileSystemLabel) { $vol.FileSystemLabel } else { "No Label" }
            Write-Host ("  [{0}:]  {1,-20}  Total: {2,8} GB  |  Free: {3,8} GB" -f
                $vol.DriveLetter, $label, $sizeGB, $freeGB) -ForegroundColor White
        }
        Write-Host "  ────────────────────────────────────────────────────────" -ForegroundColor Gray
        Write-Host ""
        return $volumes
    }
    
    function Get-RecoveryDrive {
        $volumes = Show-AvailablePartitions
        if ($null -eq $volumes) { return $null }
        do {
            $inputDrive = Read-Host "  Enter Recovery partition drive letter (e.g., R, D, E)"
            $inputDrive = $inputDrive.Trim().ToUpper().TrimEnd(':')
            $selectedVol = $volumes | Where-Object { $_.DriveLetter -eq $inputDrive }
            if (-not $selectedVol) { Write-Log "Invalid drive letter. Try again." "WARNING" }
        } while (-not $selectedVol)
        return "${inputDrive}:"
    }
    
    function Test-Prerequisites {
        param([string]$RecoveryDrive)
    
        Write-Log "Running prerequisite checks..." "INFO"
        Write-Host ""
        $allPassed = $true
    
        # Admin check
        $identity  = [Security.Principal.WindowsIdentity]::GetCurrent()
        $principal = New-Object Security.Principal.WindowsPrincipal($identity)
        if ($principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
            Write-Host "  [✓] Administrator privileges" -ForegroundColor Green
        } else {
            Write-Host "  [✗] Administrator privileges" -ForegroundColor Red
            $allPassed = $false
        }
    
        # DISM check
        if (Test-Path "$env:SystemRoot\System32\Dism.exe") {
            Write-Host "  [✓] DISM.exe found" -ForegroundColor Green
        } else {
            Write-Host "  [✗] DISM.exe not found" -ForegroundColor Red
            $allPassed = $false
        }
    
        # Recovery drive check
        if (Test-Path "$RecoveryDrive\") {
            Write-Host "  [✓] Recovery drive ($RecoveryDrive) accessible" -ForegroundColor Green
        } else {
            Write-Host "  [✗] Recovery drive ($RecoveryDrive) not accessible" -ForegroundColor Red
            $allPassed = $false
        }
    
        # VSS check
        $vssSvc = Get-Service -Name VSS -ErrorAction SilentlyContinue
        if ($vssSvc) {
            Write-Host "  [✓] VSS Service available ($($vssSvc.Status))" -ForegroundColor Green
        } else {
            Write-Host "  [✗] VSS Service not found" -ForegroundColor Red
            $allPassed = $false
        }
    
        # Free space check
        $recVol = Get-Volume -DriveLetter ($RecoveryDrive.TrimEnd(':')) -ErrorAction SilentlyContinue
        $srcVol = Get-Volume -DriveLetter 'C' -ErrorAction SilentlyContinue
        if ($recVol -and $srcVol) {
            $usedGB     = [math]::Round(($srcVol.Size - $srcVol.SizeRemaining) / 1GB, 2)
            $requiredGB = [math]::Round($usedGB * 0.7, 2)
            $freeGB     = [math]::Round($recVol.SizeRemaining / 1GB, 2)
            if ($freeGB -ge $requiredGB) {
                Write-Host "  [✓] Free space OK (Need ~${requiredGB}GB, Have ${freeGB}GB)" -ForegroundColor Green
            } else {
                Write-Host "  [✗] Insufficient space (Need ~${requiredGB}GB, Have ${freeGB}GB)" -ForegroundColor Red
                $allPassed = $false
            }
            Write-Host "  [i] C: used space: ${usedGB} GB" -ForegroundColor Gray
        }
    
        Write-Host ""
        return $allPassed
    }
    
    # ============================================================
    # VSS SHADOW COPY
    # ============================================================
    
    function New-ShadowCopy {
        Write-Log "Creating VSS Shadow Copy of C: drive..." "INFO"
    
        Start-Service VSS -ErrorAction SilentlyContinue
        Start-Service swprv -ErrorAction SilentlyContinue
        Start-Sleep -Seconds 3
    
        try {
            $result = (Get-WmiObject -List Win32_ShadowCopy).Create("C:\", "ClientAccessible")
    
            if ($result.ReturnValue -ne 0) {
                Write-Log "Shadow copy failed with code: $($result.ReturnValue)" "ERROR"
                return $null
            }
    
            $shadowID   = $result.ShadowID
            $shadowCopy = Get-WmiObject Win32_ShadowCopy | Where-Object { $_.ID -eq $shadowID }
            $shadowPath = $shadowCopy.DeviceObject
    
            Write-Log "Shadow copy created: $shadowPath" "SUCCESS"
            return @{
                ID         = $shadowID
                DevicePath = $shadowPath
            }
        }
        catch {
            Write-Log "Shadow copy exception: $_" "ERROR"
            return $null
        }
    }
    
    function Mount-ShadowCopy {
        param([string]$ShadowDevicePath, [string]$MountPoint)
    
        if (Test-Path $MountPoint) {
            cmd /c "rmdir `"$MountPoint`"" 2>$null
        }
    
        $deviceWithSlash = $ShadowDevicePath
        if (-not $deviceWithSlash.EndsWith('\')) {
            $deviceWithSlash += '\'
        }
    
        cmd /c "mklink /d `"$MountPoint`" `"$deviceWithSlash`"" 2>&1 | Out-Null
    
        if ((Test-Path $MountPoint) -and (Get-ChildItem $MountPoint -ErrorAction SilentlyContinue)) {
            Write-Log "Shadow copy mounted at $MountPoint" "SUCCESS"
            return $true
        }
    
        Write-Log "Shadow copy mount failed!" "ERROR"
        return $false
    }
    
    function Remove-ShadowCopyAndMount {
        param([string]$ShadowID, [string]$MountPoint)
    
        if (Test-Path $MountPoint) {
            cmd /c "rmdir `"$MountPoint`"" 2>$null
        }
    
        if ($ShadowID) {
            try {
                $shadow = Get-WmiObject Win32_ShadowCopy | Where-Object { $_.ID -eq $ShadowID }
                if ($shadow) { $shadow.Delete() }
                Write-Log "Shadow copy cleaned up." "SUCCESS"
            }
            catch {
                Write-Log "Shadow cleanup warning: $_" "WARNING"
            }
        }
    }
    
    # ============================================================
    # STEP 1: CAPTURE WIM IMAGE
    # ============================================================
    
    function Invoke-Step1_CaptureWIM {
        param(
            [string]$RecoveryDrive,
            [string]$FolderName,
            [string]$Name,
            [string]$Description,
            [string]$Compression
        )
    
        Write-Log "STEP 1: CAPTURE WIM IMAGE (via VSS Shadow Copy)" "STEP"
    
        $recoveryPath = Join-Path $RecoveryDrive $FolderName
        $wimFile      = Join-Path $recoveryPath "install.wim"
        $shadowInfo   = $null
    
        try {
            # Create folder
            if (-not (Test-Path $recoveryPath)) {
                New-Item -Path $recoveryPath -ItemType Directory -Force | Out-Null
                Write-Log "Created folder: $recoveryPath" "INFO"
            }
    
            # Check existing WIM
            if (Test-Path $wimFile) {
                $fileSize = [math]::Round((Get-Item $wimFile).Length / 1GB, 2)
                Write-Host ""
                Write-Host "  ⚠  Existing WIM found: $wimFile ($fileSize GB)" -ForegroundColor Yellow
                $overwrite = Read-Host "  Overwrite? (Y/N)"
                if ($overwrite -ne 'Y' -and $overwrite -ne 'y') {
                    Write-Log "Using existing WIM file." "WARNING"
                    return @{ Success = $true; WimFile = $wimFile; Skipped = $true }
                }
                Remove-Item $wimFile -Force
            }
    
            # Create shadow copy
            $shadowInfo = New-ShadowCopy
            if ($null -eq $shadowInfo) {
                return @{ Success = $false; WimFile = $null; Skipped = $false }
            }
    
            # Mount shadow copy
            $mounted = Mount-ShadowCopy -ShadowDevicePath $shadowInfo.DevicePath -MountPoint $ShadowMountPoint
            if (-not $mounted) {
                return @{ Success = $false; WimFile = $null; Skipped = $false }
            }
    
            # Capture
            Write-Host ""
            Write-Host "  ⏳ Capturing image... This takes 20-60 minutes." -ForegroundColor Yellow
            Write-Host "  ⚠  Do NOT restart or shut down your PC." -ForegroundColor Yellow
            Write-Host ""
    
            $startTime = Get-Date
    
            $batchFile = "$env:TEMP\dism_capture.cmd"
            @"
    @echo off
    DISM.exe /Capture-Image /ImageFile:"$wimFile" /CaptureDir:"$ShadowMountPoint" /Name:"$Name" /Description:"$Description" /Compress:$Compression
    exit /b %errorlevel%
    "@ | Out-File -FilePath $batchFile -Encoding ASCII -Force
    
            $proc = Start-Process -FilePath "cmd.exe" `
                                  -ArgumentList "/c `"$batchFile`"" `
                                  -NoNewWindow -Wait -PassThru
    
            $duration = (Get-Date) - $startTime
            Remove-Item $batchFile -Force -ErrorAction SilentlyContinue
    
            if ($proc.ExitCode -eq 0 -and (Test-Path $wimFile)) {
                $wimSize = [math]::Round((Get-Item $wimFile).Length / 1GB, 2)
    
                Write-Host ""
                Write-Host "  ┌──────────────────────────────────────────────────┐" -ForegroundColor Green
                Write-Host "  │  ✅ CAPTURE SUCCESSFUL!                           │" -ForegroundColor Green
                Write-Host "  │  File     : $wimFile" -ForegroundColor Green
                Write-Host "  │  Size     : $wimSize GB" -ForegroundColor Green
                Write-Host "  │  Duration : $($duration.ToString('hh\:mm\:ss'))" -ForegroundColor Green
                Write-Host "  └──────────────────────────────────────────────────┘" -ForegroundColor Green
                Write-Host ""
    
                # Verify
                Write-Log "Verifying captured image..." "INFO"
                & DISM.exe /Get-ImageInfo /ImageFile:"$wimFile"
    
                return @{ Success = $true; WimFile = $wimFile; Skipped = $false }
            }
            else {
                Write-Log "DISM capture FAILED! Exit code: $($proc.ExitCode)" "ERROR"
                Write-Host "  Check DISM log: notepad C:\Windows\Logs\DISM\dism.log" -ForegroundColor Yellow
                return @{ Success = $false; WimFile = $null; Skipped = $false }
            }
        }
        catch {
            Write-Log "Exception during capture: $_" "ERROR"
            return @{ Success = $false; WimFile = $null; Skipped = $false }
        }
        finally {
            # Always clean up shadow copy
            Write-Log "Cleaning up shadow copy..." "INFO"
            if ($shadowInfo) {
                Remove-ShadowCopyAndMount -ShadowID $shadowInfo.ID -MountPoint $ShadowMountPoint
            }
            elseif (Test-Path $ShadowMountPoint) {
                cmd /c "rmdir `"$ShadowMountPoint`"" 2>$null
            }
        }
    }
    
    # ============================================================
    # STEP 1.5: UNATTEND.XML GENERATION
    # ============================================================
    
    function Get-CurrentSystemLocale {
        $culture  = Get-Culture
        $uiLang   = Get-WinSystemLocale
        $timezone = (Get-TimeZone).Id
        $keyboard = (Get-WinUserLanguageList)[0].InputMethodTips[0]
        $kbLayout = if ($keyboard) { $keyboard } else { "0409:00000409" }
    
        return @{
            SystemLocale = $uiLang.Name
            UILanguage   = $culture.Name
            UserLocale   = $culture.Name
            InputLocale  = $kbLayout
            TimeZone     = $timezone
            LanguageName = $culture.DisplayName
        }
    }
    
    function Get-UnattendSettings {
        Write-Log "STEP 1.5: CONFIGURE SILENT OOBE (unattend.xml)" "STEP"
    
        $currentLocale = Get-CurrentSystemLocale
    
        Write-Host ""
        Write-Host "  ── Detected System Settings ──" -ForegroundColor Yellow
        Write-Host "    Language  : $($currentLocale.LanguageName)" -ForegroundColor Gray
        Write-Host "    Locale    : $($currentLocale.SystemLocale)" -ForegroundColor Gray
        Write-Host "    Keyboard  : $($currentLocale.InputLocale)" -ForegroundColor Gray
        Write-Host "    Time Zone : $($currentLocale.TimeZone)" -ForegroundColor Gray
        Write-Host ""
    
        $useCurrentLocale = Read-Host "  Use current region/keyboard/language? (Y/N) [Y]"
        if ([string]::IsNullOrEmpty($useCurrentLocale)) { $useCurrentLocale = 'Y' }
    
        if ($useCurrentLocale -eq 'Y' -or $useCurrentLocale -eq 'y') {
            $locale      = $currentLocale.SystemLocale
            $uiLanguage  = $currentLocale.UILanguage
            $userLocale  = $currentLocale.UserLocale
            $inputLocale = $currentLocale.InputLocale
            $timeZone    = $currentLocale.TimeZone
        }
        else {
            Write-Host "  Common codes: en-US, fr-FR, de-DE, es-ES, nl-NL, pt-BR, ja-JP" -ForegroundColor Gray
            $locale      = Read-Host "  System Locale (e.g., fr-FR)"
            $uiLanguage  = Read-Host "  UI Language (e.g., fr-FR)"
            $userLocale  = Read-Host "  User Locale (e.g., fr-FR)"
            $inputLocale = Read-Host "  Keyboard (e.g., 040c:0000040c for French)"
            $timeZone    = Read-Host "  Time Zone (e.g., Romance Standard Time)"
        }
    
        Write-Host ""
        Write-Host "  ── Local User Account ──" -ForegroundColor Yellow
    
        $currentUser = $env:USERNAME
        $username    = Read-Host "  Username [$currentUser]"
        if ([string]::IsNullOrEmpty($username)) { $username = $currentUser }
    
        $password    = Read-Host "  Password (leave blank for no password)"
        $hasPassword = -not [string]::IsNullOrEmpty($password)
    
        $isAdmin     = Read-Host "  Make Administrator? (Y/N) [Y]"
        if ([string]::IsNullOrEmpty($isAdmin)) { $isAdmin = 'Y' }
        $groupName   = if ($isAdmin -eq 'Y' -or $isAdmin -eq 'y') { "Administrators" } else { "Users" }
    
        $currentPC    = $env:COMPUTERNAME
        $computerName = Read-Host "  Computer Name [$currentPC]"
        if ([string]::IsNullOrEmpty($computerName)) { $computerName = $currentPC }
    
        $autoLogon = Read-Host "  Auto-login after restore? (Y/N) [Y]"
        if ([string]::IsNullOrEmpty($autoLogon)) { $autoLogon = 'Y' }
    
        # Summary
        Write-Host ""
        Write-Host "  ┌──────────────────────────────────────────────────┐" -ForegroundColor White
        Write-Host "  │  Locale     : $locale" -ForegroundColor White
        Write-Host "  │  Keyboard   : $inputLocale" -ForegroundColor White
        Write-Host "  │  Time Zone  : $timeZone" -ForegroundColor White
        Write-Host "  │  Username   : $username" -ForegroundColor White
        Write-Host "  │  Password   : $(if ($hasPassword) {'****'} else {'(none)'})" -ForegroundColor White
        Write-Host "  │  Admin      : $groupName" -ForegroundColor White
        Write-Host "  │  PC Name    : $computerName" -ForegroundColor White
        Write-Host "  │  Auto-login : $autoLogon" -ForegroundColor White
        Write-Host "  └──────────────────────────────────────────────────┘" -ForegroundColor White
        Write-Host ""
    
        $confirm = Read-Host "  Confirm these settings? (Y/N)"
        if ($confirm -ne 'Y' -and $confirm -ne 'y') { return $null }
    
        return @{
            Locale       = $locale
            UILanguage   = $uiLanguage
            UserLocale   = $userLocale
            InputLocale  = $inputLocale
            TimeZone     = $timeZone
            Username     = $username
            Password     = $password
            HasPassword  = $hasPassword
            GroupName    = $groupName
            ComputerName = $computerName
            AutoLogon    = ($autoLogon -eq 'Y' -or $autoLogon -eq 'y')
        }
    }
    
    function New-UnattendXml {
        param([hashtable]$S)
    
        $arch = if ([Environment]::Is64BitOperatingSystem) { "amd64" } else { "x86" }
    
        $passwordBlock = if ($S.HasPassword) {
            $encodedPwd = [Convert]::ToBase64String(
                [System.Text.Encoding]::Unicode.GetBytes($S.Password + "Password")
            )
            "<Password><Value>$encodedPwd</Value><PlainText>false</PlainText></Password>"
        } else {
            "<Password><Value></Value><PlainText>true</PlainText></Password>"
        }
    
        $autoLogonBlock = ""
        if ($S.AutoLogon) {
            $autoLogonBlock = @"
                    <AutoLogon>
                        <Enabled>true</Enabled>
                        <Username>$($S.Username)</Username>
                        <Password>
                            <Value>$($S.Password)</Value>
                            <PlainText>true</PlainText>
                        </Password>
                        <LogonCount>3</LogonCount>
                    </AutoLogon>
    "@
        }
    
        return @"
    <?xml version="1.0" encoding="utf-8"?>
    <unattend xmlns="urn:schemas-microsoft-com:unattend"
              xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
        <settings pass="specialize">
            <component name="Microsoft-Windows-Shell-Setup"
                       processorArchitecture="$arch"
                       publicKeyToken="31bf3856ad364e35"
                       language="neutral" versionScope="nonSxS">
                <ComputerName>$($S.ComputerName)</ComputerName>
            </component>
            <component name="Microsoft-Windows-Deployment"
                       processorArchitecture="$arch"
                       publicKeyToken="31bf3856ad364e35"
                       language="neutral" versionScope="nonSxS">
                <RunSynchronous>
                    <RunSynchronousCommand wcm:action="add">
                        <Order>1</Order>
                        <Path>reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE /v BypassNRO /t REG_DWORD /d 1 /f</Path>
                    </RunSynchronousCommand>
                </RunSynchronous>
            </component>
        </settings>
        <settings pass="oobeSystem">
            <component name="Microsoft-Windows-International-Core"
                       processorArchitecture="$arch"
                       publicKeyToken="31bf3856ad364e35"
                       language="neutral" versionScope="nonSxS">
                <InputLocale>$($S.InputLocale)</InputLocale>
                <SystemLocale>$($S.Locale)</SystemLocale>
                <UILanguage>$($S.UILanguage)</UILanguage>
                <UILanguageFallback>en-US</UILanguageFallback>
                <UserLocale>$($S.UserLocale)</UserLocale>
            </component>
            <component name="Microsoft-Windows-Shell-Setup"
                       processorArchitecture="$arch"
                       publicKeyToken="31bf3856ad364e35"
                       language="neutral" versionScope="nonSxS">
                <OOBE>
                    <HideEULAPage>true</HideEULAPage>
                    <HideLocalAccountScreen>true</HideLocalAccountScreen>
                    <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
                    <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
                    <NetworkLocation>Work</NetworkLocation>
                    <ProtectYourPC>3</ProtectYourPC>
                    <SkipMachineOOBE>true</SkipMachineOOBE>
                    <SkipUserOOBE>true</SkipUserOOBE>
                </OOBE>
                <TimeZone>$($S.TimeZone)</TimeZone>
                <UserAccounts>
                    <LocalAccounts>
                        <LocalAccount wcm:action="add">
                            <Name>$($S.Username)</Name>
                            <Group>$($S.GroupName)</Group>
                            <DisplayName>$($S.Username)</DisplayName>
                            $passwordBlock
                        </LocalAccount>
                    </LocalAccounts>
                </UserAccounts>
    $autoLogonBlock
                <FirstLogonCommands>
                    <SynchronousCommand wcm:action="add">
                        <Order>1</Order>
                        <CommandLine>reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</CommandLine>
                        <RequiresUserInput>false</RequiresUserInput>
                    </SynchronousCommand>
                    <SynchronousCommand wcm:action="add">
                        <Order>2</Order>
                        <CommandLine>reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\OOBE" /v DisablePrivacyExperience /t REG_DWORD /d 1 /f</CommandLine>
                        <RequiresUserInput>false</RequiresUserInput>
                    </SynchronousCommand>
                </FirstLogonCommands>
            </component>
        </settings>
    </unattend>
    "@
    }
    
    function Save-UnattendXml {
        param([string]$RecoveryPath, [hashtable]$Settings)
    
        if ($null -eq $Settings) {
            Write-Log "Unattend configuration skipped." "WARNING"
            return
        }
    
        $xml = New-UnattendXml -S $Settings
        $unattendFile = Join-Path $RecoveryPath "unattend.xml"
        $xml | Out-File -FilePath $unattendFile -Encoding UTF8 -Force
        Write-Log "Saved: $unattendFile" "SUCCESS"
    }
    
    # ============================================================
    # STEP 2: CREATE RECOVERY SCRIPTS
    # ════════════════════════════════════════════════════════════
    # Key design:
    # - NO setlocal EnableDelayedExpansion
    # - NO !variables! in any DISM/diskpart commands
    # - ALL paths hardcoded at generation time
    # - ping instead of timeout (WinRE compatible)
    # - Separate diskpart calls for re-hide
    # - Full logging to C:\temp\restore_log.txt
    # ============================================================
    
    function Invoke-Step2_CreateRecoveryScripts {
        param(
            [string]$RecoveryDrive,
            [string]$RecoveryPath,
            [string]$WimFile
        )
    
        Write-Log "STEP 2: CREATE RECOVERY SCRIPTS" "STEP"
    
        $diskNumber = (Get-Partition -DriveLetter C).DiskNumber
        $cPartNum   = (Get-Partition -DriveLetter C).PartitionNumber
        $partitions = Get-Partition -DiskNumber $diskNumber
        $efiPart    = $partitions | Where-Object {
            $_.Type -eq 'System' -or $_.GptType -eq '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}'
        }
        $efiPartNum = if ($efiPart) { $efiPart.PartitionNumber } else { 1 }
    
        $recDriveLetter = $RecoveryDrive.TrimEnd(':')
        $recPartition   = Get-Partition -DriveLetter $recDriveLetter
        $recPartNum     = $recPartition.PartitionNumber
        $recDiskNum     = $recPartition.DiskNumber
        $osPartSize     = [math]::Round((Get-Partition -DriveLetter C).Size / 1GB, 0)
    
        Write-Host ""
        Write-Host "  ── Partition Layout ──" -ForegroundColor Yellow
        Write-Host "    OS Disk       : $diskNumber, Partition $cPartNum (C:)" -ForegroundColor Gray
        Write-Host "    EFI           : $diskNumber, Partition $efiPartNum" -ForegroundColor Gray
        Write-Host "    Recovery      : $recDiskNum, Partition $recPartNum" -ForegroundColor Gray
        Write-Host ""
    
        # ════════════════════════════════════════════════════════
        #  RESTORE.CMD
        #
        #  Fix: Scans ALL drive letters first to find install.wim
        #  Only uses diskpart if WIM not found on any letter
        # ════════════════════════════════════════════════════════
    
        $restoreScript = @"
    @echo off
    cls
    color 1F
    
    mkdir C:\temp 2>nul
    
    echo ================================================== > C:\temp\restore_log.txt
    echo  FACTORY RESET LOG >> C:\temp\restore_log.txt
    echo  Started: %date% %time% >> C:\temp\restore_log.txt
    echo ================================================== >> C:\temp\restore_log.txt
    
    echo.
    echo  ========================================================
    echo   FACTORY RESET - FULL SYSTEM RESTORE
    echo   All data on C: will be PERMANENTLY DELETED!
    echo   Log: C:\temp\restore_log.txt
    echo  ========================================================
    echo.
    
    if /i "%1"=="AUTO" goto SKIP_CONFIRM
    
    echo  Type YES to confirm factory reset:
    set /p CONFIRM=
    if /i not "%CONFIRM%"=="YES" (
        echo  Cancelled.
        echo  [%time%] Cancelled >> C:\temp\restore_log.txt
        pause
        exit /b 0
    )
    
    :SKIP_CONFIRM
    echo  [%time%] Confirmed >> C:\temp\restore_log.txt
    
    REM ──────────────────────────────────────────────────────
    REM  STEP 1/6: FIND the recovery image
    REM
    REM  Method A: Scan all existing drive letters (D-Z)
    REM            WinRE may have already assigned a letter
    REM  Method B: Use diskpart to mount known partition as R:
    REM            For when partition is truly hidden
    REM ──────────────────────────────────────────────────────
    
    echo.
    echo  [1/6] Searching for recovery image...
    echo.
    echo  [%time%] STEP 1: Finding recovery image >> C:\temp\restore_log.txt
    
    set RECOVERY_DRIVE=
    set WIM_FOUND=0
    
    REM ── Method A: Scan all existing drive letters ──
    echo  Scanning all drive letters...
    echo  [%time%] Scanning drive letters A-Z >> C:\temp\restore_log.txt
    
    if exist C:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=C:
        set WIM_FOUND=1
        echo  Found on C:
        echo  [%time%] Found on C: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist D:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=D:
        set WIM_FOUND=1
        echo  Found on D:
        echo  [%time%] Found on D: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist E:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=E:
        set WIM_FOUND=1
        echo  Found on E:
        echo  [%time%] Found on E: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist F:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=F:
        set WIM_FOUND=1
        echo  Found on F:
        echo  [%time%] Found on F: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist G:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=G:
        set WIM_FOUND=1
        echo  Found on G:
        echo  [%time%] Found on G: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist H:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=H:
        set WIM_FOUND=1
        echo  Found on H:
        echo  [%time%] Found on H: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist I:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=I:
        set WIM_FOUND=1
        echo  Found on I:
        echo  [%time%] Found on I: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist J:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=J:
        set WIM_FOUND=1
        echo  Found on J:
        echo  [%time%] Found on J: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist K:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=K:
        set WIM_FOUND=1
        echo  Found on K:
        echo  [%time%] Found on K: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist L:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=L:
        set WIM_FOUND=1
        echo  Found on L:
        echo  [%time%] Found on L: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist M:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=M:
        set WIM_FOUND=1
        echo  Found on M:
        echo  [%time%] Found on M: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist N:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=N:
        set WIM_FOUND=1
        echo  Found on N:
        echo  [%time%] Found on N: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist O:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=O:
        set WIM_FOUND=1
        echo  Found on O:
        echo  [%time%] Found on O: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist P:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=P:
        set WIM_FOUND=1
        echo  Found on P:
        echo  [%time%] Found on P: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist Q:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=Q:
        set WIM_FOUND=1
        echo  Found on Q:
        echo  [%time%] Found on Q: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist R:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=R:
        set WIM_FOUND=1
        echo  Found on R:
        echo  [%time%] Found on R: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist S:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=S:
        set WIM_FOUND=1
        echo  Found on S:
        echo  [%time%] Found on S: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist T:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=T:
        set WIM_FOUND=1
        echo  Found on T:
        echo  [%time%] Found on T: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist U:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=U:
        set WIM_FOUND=1
        echo  Found on U:
        echo  [%time%] Found on U: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist V:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=V:
        set WIM_FOUND=1
        echo  Found on V:
        echo  [%time%] Found on V: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist W:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=W:
        set WIM_FOUND=1
        echo  Found on W:
        echo  [%time%] Found on W: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist X:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=X:
        set WIM_FOUND=1
        echo  Found on X:
        echo  [%time%] Found on X: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist Y:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=Y:
        set WIM_FOUND=1
        echo  Found on Y:
        echo  [%time%] Found on Y: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    if exist Z:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=Z:
        set WIM_FOUND=1
        echo  Found on Z:
        echo  [%time%] Found on Z: >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    
    REM ── Method B: Not found on any letter — mount via diskpart ──
    echo.
    echo  Not found on any lettered drive.
    echo  Mounting hidden partition (Disk $recDiskNum, Part $recPartNum) as R: ...
    echo  [%time%] Not on any letter. Trying diskpart mount... >> C:\temp\restore_log.txt
    
    (
    echo select disk $recDiskNum
    echo select partition $recPartNum
    echo assign letter=R
    echo exit
    ) > %TEMP%\dp_mount.txt
    
    diskpart /s %TEMP%\dp_mount.txt >> C:\temp\restore_log.txt 2>&1
    echo  [%time%] diskpart mount exit: %errorlevel% >> C:\temp\restore_log.txt
    del %TEMP%\dp_mount.txt 2>nul
    
    ping -n 5 127.0.0.1 > nul
    
    if exist R:\$RecoveryFolderName\install.wim (
        set RECOVERY_DRIVE=R:
        set WIM_FOUND=1
        echo  Found after diskpart mount on R:
        echo  [%time%] Found on R: after diskpart >> C:\temp\restore_log.txt
        goto FOUND_WIM
    )
    
    REM ── ALL METHODS FAILED ──
    echo.
    echo  ════════════════════════════════════════════════════
    echo   ERROR: Recovery image not found anywhere!
    echo  ════════════════════════════════════════════════════
    echo.
    echo  [%time%] FATAL: Recovery image not found! >> C:\temp\restore_log.txt
    echo  [%time%] Dumping volume list: >> C:\temp\restore_log.txt
    (echo list volume
    echo exit) > %TEMP%\dp_dbg.txt
    diskpart /s %TEMP%\dp_dbg.txt >> C:\temp\restore_log.txt 2>&1
    diskpart /s %TEMP%\dp_dbg.txt
    del %TEMP%\dp_dbg.txt 2>nul
    echo.
    echo  Check log: C:\temp\restore_log.txt
    pause
    exit /b 1
    
    :FOUND_WIM
    echo.
    echo  Recovery drive : %RECOVERY_DRIVE%
    echo  Image file     : %RECOVERY_DRIVE%\$RecoveryFolderName\install.wim
    echo.
    echo  [%time%] Using drive: %RECOVERY_DRIVE% >> C:\temp\restore_log.txt
    
    REM ──────────────────────────────────────────────────────
    REM  STEP 2/6: Format C:
    REM ──────────────────────────────────────────────────────
    
    echo  [2/6] Formatting C: drive...
    echo.
    echo  [%time%] STEP 2: Formatting C: >> C:\temp\restore_log.txt
    
    REM ── Save log before format ──
    mkdir %RECOVERY_DRIVE%\temp 2>nul
    copy /y C:\temp\restore_log.txt %RECOVERY_DRIVE%\temp\restore_log.txt > nul 2>nul
    
    format C: /FS:NTFS /Q /Y /V:Windows
    set FMT_ERR=%errorlevel%
    
    REM ── Restore log after format ──
    mkdir C:\temp 2>nul
    copy /y %RECOVERY_DRIVE%\temp\restore_log.txt C:\temp\restore_log.txt > nul 2>nul
    echo  [%time%] format exit: %FMT_ERR% >> C:\temp\restore_log.txt
    
    if %FMT_ERR% neq 0 (
        echo  format.exe failed, using diskpart...
        echo  [%time%] format failed, trying diskpart >> C:\temp\restore_log.txt
    
        (
        echo select disk $diskNumber
        echo select partition $cPartNum
        echo format fs=ntfs quick label=Windows
        echo assign letter=C
        echo exit
        ) > %TEMP%\dp_fmt.txt
    
        diskpart /s %TEMP%\dp_fmt.txt >> C:\temp\restore_log.txt 2>&1
        del %TEMP%\dp_fmt.txt 2>nul
    
        mkdir C:\temp 2>nul
        copy /y %RECOVERY_DRIVE%\temp\restore_log.txt C:\temp\restore_log.txt > nul 2>nul
    )
    
    echo  C: formatted.
    echo  [%time%] Format done >> C:\temp\restore_log.txt
    
    REM ──────────────────────────────────────────────────────
    REM  STEP 3/6: Apply recovery image
    REM
    REM  Uses %RECOVERY_DRIVE% which was set during scan.
    REM  This is a SIMPLE %var% — works fine in basic cmd.exe
    REM  (only !var! delayed expansion is broken in WinRE)
    REM ──────────────────────────────────────────────────────
    
    echo.
    echo  [3/6] Applying recovery image (15-30 minutes)...
    echo  DO NOT turn off your computer!
    echo.
    echo  [%time%] STEP 3: Applying image >> C:\temp\restore_log.txt
    echo  [%time%] Source: %RECOVERY_DRIVE%\$RecoveryFolderName\install.wim >> C:\temp\restore_log.txt
    
    DISM.exe /Apply-Image /ImageFile:%RECOVERY_DRIVE%\$RecoveryFolderName\install.wim /Index:1 /ApplyDir:C:\
    set DISM_ERR=%errorlevel%
    echo  [%time%] DISM exit: %DISM_ERR% >> C:\temp\restore_log.txt
    
    if %DISM_ERR% neq 0 (
        echo.
        echo  Attempt 1 failed (code: %DISM_ERR%).
        echo  Trying without trailing backslash...
        echo  [%time%] Trying /ApplyDir:C: >> C:\temp\restore_log.txt
    
        DISM.exe /Apply-Image /ImageFile:%RECOVERY_DRIVE%\$RecoveryFolderName\install.wim /Index:1 /ApplyDir:C:
        set DISM_ERR2=%errorlevel%
        echo  [%time%] DISM attempt 2 exit: %DISM_ERR2% >> C:\temp\restore_log.txt
    
        if %DISM_ERR2% neq 0 (
            echo.
            echo  ALL DISM ATTEMPTS FAILED!
            echo  [%time%] ALL DISM FAILED >> C:\temp\restore_log.txt
            echo.
            echo  Source: %RECOVERY_DRIVE%\$RecoveryFolderName\install.wim
            echo  Target: C:\
            echo  Log: C:\temp\restore_log.txt
            pause
            exit /b 1
        )
    )
    
    echo.
    echo  Image applied successfully!
    echo  [%time%] Image applied OK >> C:\temp\restore_log.txt
    
    REM ──────────────────────────────────────────────────────
    REM  STEP 4/6: Rebuild boot
    REM ──────────────────────────────────────────────────────
    
    echo.
    echo  [4/6] Rebuilding boot configuration...
    echo.
    echo  [%time%] STEP 4: Boot config >> C:\temp\restore_log.txt
    
    (
    echo select disk $diskNumber
    echo select partition $efiPartNum
    echo assign letter=S
    echo exit
    ) > %TEMP%\dp_efi.txt
    
    diskpart /s %TEMP%\dp_efi.txt >> C:\temp\restore_log.txt 2>&1
    del %TEMP%\dp_efi.txt 2>nul
    
    ping -n 3 127.0.0.1 > nul
    
    echo  [%time%] bcdboot C:\Windows /s S: /f UEFI >> C:\temp\restore_log.txt
    bcdboot C:\Windows /s S: /f UEFI >> C:\temp\restore_log.txt 2>&1
    set BCD_ERR=%errorlevel%
    echo  [%time%] bcdboot exit: %BCD_ERR% >> C:\temp\restore_log.txt
    
    if %BCD_ERR% neq 0 (
        echo  [%time%] Trying without /s S: >> C:\temp\restore_log.txt
        bcdboot C:\Windows /f UEFI >> C:\temp\restore_log.txt 2>&1
    )
    
    (
    echo select disk $diskNumber
    echo select partition $efiPartNum
    echo remove letter=S
    echo exit
    ) > %TEMP%\dp_efi2.txt
    
    diskpart /s %TEMP%\dp_efi2.txt >> C:\temp\restore_log.txt 2>&1
    del %TEMP%\dp_efi2.txt 2>nul
    
    echo  Boot rebuilt.
    echo  [%time%] Boot done >> C:\temp\restore_log.txt
    
    REM ──────────────────────────────────────────────────────
    REM  STEP 5/6: Unattend.xml
    REM ──────────────────────────────────────────────────────
    
    echo.
    echo  [5/6] Applying unattend.xml...
    echo.
    echo  [%time%] STEP 5: Unattend >> C:\temp\restore_log.txt
    
    if exist %RECOVERY_DRIVE%\$RecoveryFolderName\unattend.xml (
        echo  [%time%] unattend.xml found >> C:\temp\restore_log.txt
    
        mkdir C:\Windows\Panther\unattend 2>nul
        mkdir C:\Windows\System32\Sysprep 2>nul
    
        copy /y %RECOVERY_DRIVE%\$RecoveryFolderName\unattend.xml C:\Windows\Panther\unattend\unattend.xml >> C:\temp\restore_log.txt 2>&1
        copy /y %RECOVERY_DRIVE%\$RecoveryFolderName\unattend.xml C:\Windows\Panther\unattend.xml >> C:\temp\restore_log.txt 2>&1
        copy /y %RECOVERY_DRIVE%\$RecoveryFolderName\unattend.xml C:\Windows\System32\Sysprep\unattend.xml >> C:\temp\restore_log.txt 2>&1
    
        reg load HKLM\OFFLINE_SW C:\Windows\System32\config\SOFTWARE >> C:\temp\restore_log.txt 2>&1
        reg add "HKLM\OFFLINE_SW\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f >> C:\temp\restore_log.txt 2>&1
        reg add "HKLM\OFFLINE_SW\Policies\Microsoft\Windows\OOBE" /v DisablePrivacyExperience /t REG_DWORD /d 1 /f >> C:\temp\restore_log.txt 2>&1
        reg unload HKLM\OFFLINE_SW >> C:\temp\restore_log.txt 2>&1
    
        echo  Unattend.xml applied.
        echo  [%time%] Unattend OK >> C:\temp\restore_log.txt
    ) else (
        echo  No unattend.xml found.
        echo  [%time%] No unattend.xml >> C:\temp\restore_log.txt
    )
    
    REM ──────────────────────────────────────────────────────
    REM  STEP 6/6: Re-hide recovery partition
    REM  Separate diskpart calls for reliability
    REM ──────────────────────────────────────────────────────
    
    echo.
    echo  [6/6] Re-hiding recovery partition...
    echo.
    echo  [%time%] STEP 6: Re-hide >> C:\temp\restore_log.txt
    
    REM ── 6a: Remove whatever letter recovery has ──
    echo  [%time%] 6a: Removing recovery drive letter >> C:\temp\restore_log.txt
    
    (
    echo select disk $recDiskNum
    echo select partition $recPartNum
    echo remove
    echo exit
    ) > %TEMP%\dp_rh1.txt
    
    diskpart /s %TEMP%\dp_rh1.txt >> C:\temp\restore_log.txt 2>&1
    echo  [%time%] Remove letter exit: %errorlevel% >> C:\temp\restore_log.txt
    del %TEMP%\dp_rh1.txt 2>nul
    
    ping -n 3 127.0.0.1 > nul
    
    REM ── 6b: Set partition type ──
    echo  [%time%] 6b: Setting partition type >> C:\temp\restore_log.txt
    
    (
    echo select disk $recDiskNum
    echo select partition $recPartNum
    echo set id=de94bba4-06d1-4d40-a16a-bfd50179d6ac override
    echo exit
    ) > %TEMP%\dp_rh2.txt
    
    diskpart /s %TEMP%\dp_rh2.txt >> C:\temp\restore_log.txt 2>&1
    echo  [%time%] Set type exit: %errorlevel% >> C:\temp\restore_log.txt
    del %TEMP%\dp_rh2.txt 2>nul
    
    ping -n 3 127.0.0.1 > nul
    
    REM ── 6c: Set GPT attributes ──
    echo  [%time%] 6c: Setting GPT attributes >> C:\temp\restore_log.txt
    
    (
    echo select disk $recDiskNum
    echo select partition $recPartNum
    echo gpt attributes=0x8000000000000001
    echo exit
    ) > %TEMP%\dp_rh3.txt
    
    diskpart /s %TEMP%\dp_rh3.txt >> C:\temp\restore_log.txt 2>&1
    echo  [%time%] Set GPT exit: %errorlevel% >> C:\temp\restore_log.txt
    del %TEMP%\dp_rh3.txt 2>nul
    
    echo  Re-hide complete.
    echo  [%time%] Re-hide done >> C:\temp\restore_log.txt
    
    REM ── Verify ──
    echo  [%time%] Final volume list: >> C:\temp\restore_log.txt
    (echo list volume
    echo exit) > %TEMP%\dp_vfy.txt
    diskpart /s %TEMP%\dp_vfy.txt >> C:\temp\restore_log.txt 2>&1
    del %TEMP%\dp_vfy.txt 2>nul
    
    REM ──────────────────────────────────────────────────────
    REM  DONE
    REM ──────────────────────────────────────────────────────
    
    echo. >> C:\temp\restore_log.txt
    echo ================================================== >> C:\temp\restore_log.txt
    echo  COMPLETED: %date% %time% >> C:\temp\restore_log.txt
    echo ================================================== >> C:\temp\restore_log.txt
    
    echo.
    echo  ========================================================
    echo.
    echo   FACTORY RESET COMPLETE!
    echo   All applications, drivers, and settings restored.
    echo   Log: C:\temp\restore_log.txt
    echo.
    echo   Restarting in 15 seconds...
    echo.
    echo  ========================================================
    echo.
    
    echo  15...
    ping -n 4 127.0.0.1 > nul
    echo  12...
    ping -n 4 127.0.0.1 > nul
    echo  8...
    ping -n 5 127.0.0.1 > nul
    echo  3...
    ping -n 4 127.0.0.1 > nul
    
    wpeutil reboot
    "@
    
        # ════════════════════════════════════════════════════════
        #  DIAGNOSE.CMD
        # ════════════════════════════════════════════════════════
    
        $diagnoseScript = @"
    @echo off
    cls
    color 1F
    
    mkdir C:\temp 2>nul
    
    echo RECOVERY DIAGNOSTICS LOG > C:\temp\diagnose_log.txt
    echo %date% %time% >> C:\temp\diagnose_log.txt
    
    echo.
    echo  ========================================================
    echo   RECOVERY DIAGNOSTICS (safe - no changes)
    echo   Log: C:\temp\diagnose_log.txt
    echo  ========================================================
    echo.
    
    echo  ── Current volumes ──
    echo.
    (echo list volume
    echo exit) > %TEMP%\dd1.txt
    diskpart /s %TEMP%\dd1.txt
    diskpart /s %TEMP%\dd1.txt >> C:\temp\diagnose_log.txt 2>&1
    del %TEMP%\dd1.txt 2>nul
    
    echo.
    echo  ── Partitions on Disk $diskNumber ──
    echo.
    (echo select disk $diskNumber
    echo list partition
    echo exit) > %TEMP%\dd2.txt
    diskpart /s %TEMP%\dd2.txt
    diskpart /s %TEMP%\dd2.txt >> C:\temp\diagnose_log.txt 2>&1
    del %TEMP%\dd2.txt 2>nul
    
    echo.
    echo  ── Expected layout ──
    echo    Windows  : Disk $diskNumber, Partition $cPartNum (C:)
    echo    EFI      : Disk $diskNumber, Partition $efiPartNum
    echo    Recovery : Disk $recDiskNum, Partition $recPartNum
    
    echo.
    echo  ── Scanning ALL drives for install.wim ──
    echo.
    echo  Scanning... >> C:\temp\diagnose_log.txt
    
    if exist C:\$RecoveryFolderName\install.wim echo  FOUND on C:
    if exist D:\$RecoveryFolderName\install.wim echo  FOUND on D:
    if exist E:\$RecoveryFolderName\install.wim echo  FOUND on E:
    if exist F:\$RecoveryFolderName\install.wim echo  FOUND on F:
    if exist G:\$RecoveryFolderName\install.wim echo  FOUND on G:
    if exist H:\$RecoveryFolderName\install.wim echo  FOUND on H:
    if exist I:\$RecoveryFolderName\install.wim echo  FOUND on I:
    if exist J:\$RecoveryFolderName\install.wim echo  FOUND on J:
    if exist K:\$RecoveryFolderName\install.wim echo  FOUND on K:
    if exist R:\$RecoveryFolderName\install.wim echo  FOUND on R:
    
    echo.
    echo  ── Trying diskpart mount (Disk $recDiskNum, Part $recPartNum as R:) ──
    echo.
    (echo select disk $recDiskNum
    echo select partition $recPartNum
    echo assign letter=R
    echo exit) > %TEMP%\dd3.txt
    diskpart /s %TEMP%\dd3.txt >> C:\temp\diagnose_log.txt 2>&1
    del %TEMP%\dd3.txt 2>nul
    
    ping -n 5 127.0.0.1 > nul
    
    if exist R:\$RecoveryFolderName\install.wim (
        echo  FOUND on R: after diskpart mount
        DISM.exe /Get-ImageInfo /ImageFile:R:\$RecoveryFolderName\install.wim
    ) else (
        echo  NOT FOUND on R: after mount
        echo  R: contents:
        dir R:\ 2>nul
    )
    
    echo.
    echo  ── Cleanup ──
    (echo select disk $recDiskNum
    echo select partition $recPartNum
    echo remove
    echo exit) > %TEMP%\dd4.txt
    diskpart /s %TEMP%\dd4.txt >> C:\temp\diagnose_log.txt 2>&1
    del %TEMP%\dd4.txt 2>nul
    
    echo.
    echo  ========================================================
    echo   DONE. Log: C:\temp\diagnose_log.txt
    echo.
    echo   restore.cmd will auto-find the WIM on any drive letter.
    echo   Just run: [drive]:\$RecoveryFolderName\restore.cmd
    echo  ========================================================
    echo.
    pause
    "@
    
        # ════════════════════════════════════════════════════════
        #  QUICK_RESTORE.CMD
        # ════════════════════════════════════════════════════════
    
        $quickRestoreScript = @"
    @echo off
    cls
    mkdir C:\temp 2>nul
    echo QUICK RESTORE %date% %time% > C:\temp\quick_restore_log.txt
    
    echo  AUTOMATED FACTORY RESET...
    
    set RECOVERY_DRIVE=
    set WIM_FOUND=0
    
    REM ── Scan all letters ──
    if exist C:\$RecoveryFolderName\install.wim set RECOVERY_DRIVE=C:& set WIM_FOUND=1& goto QF
    if exist D:\$RecoveryFolderName\install.wim set RECOVERY_DRIVE=D:& set WIM_FOUND=1& goto QF
    if exist E:\$RecoveryFolderName\install.wim set RECOVERY_DRIVE=E:& set WIM_FOUND=1& goto QF
    if exist F:\$RecoveryFolderName\install.wim set RECOVERY_DRIVE=F:& set WIM_FOUND=1& goto QF
    if exist G:\$RecoveryFolderName\install.wim set RECOVERY_DRIVE=G:& set WIM_FOUND=1& goto QF
    if exist H:\$RecoveryFolderName\install.wim set RECOVERY_DRIVE=H:& set WIM_FOUND=1& goto QF
    if exist R:\$RecoveryFolderName\install.wim set RECOVERY_DRIVE=R:& set WIM_FOUND=1& goto QF
    
    REM ── Diskpart mount ──
    (echo select disk $recDiskNum
    echo select partition $recPartNum
    echo assign letter=R
    echo exit) > %TEMP%\qm.txt
    diskpart /s %TEMP%\qm.txt >> C:\temp\quick_restore_log.txt 2>&1
    del %TEMP%\qm.txt 2>nul
    ping -n 5 127.0.0.1 > nul
    
    if exist R:\$RecoveryFolderName\install.wim set RECOVERY_DRIVE=R:& set WIM_FOUND=1& goto QF
    
    echo FATAL: Not found! >> C:\temp\quick_restore_log.txt
    echo FATAL: Recovery image not found!
    pause
    exit /b 1
    
    :QF
    echo  Found on %RECOVERY_DRIVE% >> C:\temp\quick_restore_log.txt
    
    mkdir %RECOVERY_DRIVE%\temp 2>nul
    copy /y C:\temp\quick_restore_log.txt %RECOVERY_DRIVE%\temp\quick_restore_log.txt > nul 2>nul
    
    format C: /FS:NTFS /Q /Y /V:Windows
    
    if errorlevel 1 (
        (echo select disk $diskNumber
        echo select partition $cPartNum
        echo format fs=ntfs quick label=Windows
        echo assign letter=C
        echo exit) > %TEMP%\qf.txt
        diskpart /s %TEMP%\qf.txt > nul 2>&1
        del %TEMP%\qf.txt 2>nul
    )
    
    mkdir C:\temp 2>nul
    copy /y %RECOVERY_DRIVE%\temp\quick_restore_log.txt C:\temp\quick_restore_log.txt > nul 2>nul
    
    DISM.exe /Apply-Image /ImageFile:%RECOVERY_DRIVE%\$RecoveryFolderName\install.wim /Index:1 /ApplyDir:C:\
    
    if errorlevel 1 (
        DISM.exe /Apply-Image /ImageFile:%RECOVERY_DRIVE%\$RecoveryFolderName\install.wim /Index:1 /ApplyDir:C:
        if errorlevel 1 (
            echo DISM FAILED! >> C:\temp\quick_restore_log.txt
            echo CRITICAL: DISM failed!
            pause
            exit /b 1
        )
    )
    
    (echo select disk $diskNumber
    echo select partition $efiPartNum
    echo assign letter=S
    echo exit) > %TEMP%\qe.txt
    diskpart /s %TEMP%\qe.txt > nul 2>&1
    del %TEMP%\qe.txt 2>nul
    ping -n 3 127.0.0.1 > nul
    
    bcdboot C:\Windows /s S: /f UEFI > nul 2>&1
    
    if exist %RECOVERY_DRIVE%\$RecoveryFolderName\unattend.xml (
        mkdir C:\Windows\Panther\unattend 2>nul
        copy /y %RECOVERY_DRIVE%\$RecoveryFolderName\unattend.xml C:\Windows\Panther\unattend\unattend.xml
        copy /y %RECOVERY_DRIVE%\$RecoveryFolderName\unattend.xml C:\Windows\Panther\unattend.xml
        copy /y %RECOVERY_DRIVE%\$RecoveryFolderName\unattend.xml C:\Windows\System32\Sysprep\unattend.xml
        reg load HKLM\OFFLINE_SW C:\Windows\System32\config\SOFTWARE 2>nul
        reg add "HKLM\OFFLINE_SW\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f 2>nul
        reg add "HKLM\OFFLINE_SW\Policies\Microsoft\Windows\OOBE" /v DisablePrivacyExperience /t REG_DWORD /d 1 /f 2>nul
        reg unload HKLM\OFFLINE_SW 2>nul
    )
    
    (echo select disk $diskNumber
    echo select partition $efiPartNum
    echo remove letter=S
    echo exit) > %TEMP%\qe2.txt
    diskpart /s %TEMP%\qe2.txt > nul 2>&1
    del %TEMP%\qe2.txt 2>nul
    ping -n 3 127.0.0.1 > nul
    
    (echo select disk $recDiskNum
    echo select partition $recPartNum
    echo remove
    echo exit) > %TEMP%\qr1.txt
    diskpart /s %TEMP%\qr1.txt > nul 2>&1
    del %TEMP%\qr1.txt 2>nul
    ping -n 3 127.0.0.1 > nul
    
    (echo select disk $recDiskNum
    echo select partition $recPartNum
    echo set id=de94bba4-06d1-4d40-a16a-bfd50179d6ac override
    echo exit) > %TEMP%\qr2.txt
    diskpart /s %TEMP%\qr2.txt > nul 2>&1
    del %TEMP%\qr2.txt 2>nul
    ping -n 3 127.0.0.1 > nul
    
    (echo select disk $recDiskNum
    echo select partition $recPartNum
    echo gpt attributes=0x8000000000000001
    echo exit) > %TEMP%\qr3.txt
    diskpart /s %TEMP%\qr3.txt > nul 2>&1
    del %TEMP%\qr3.txt 2>nul
    
    echo COMPLETE >> C:\temp\quick_restore_log.txt
    wpeutil reboot
    "@
    
        # ── Save ──
        $restoreScript      | Out-File (Join-Path $RecoveryPath "restore.cmd") -Encoding ASCII -Force
        $diagnoseScript     | Out-File (Join-Path $RecoveryPath "diagnose.cmd") -Encoding ASCII -Force
        $quickRestoreScript | Out-File (Join-Path $RecoveryPath "quick_restore.cmd") -Encoding ASCII -Force
    
        @"
    Recovery Partition Metadata
    ===========================
    Created       : $(Get-Date)
    OS Disk       : $diskNumber
    OS Partition  : $cPartNum (C:, ~${osPartSize} GB)
    EFI Partition : $efiPartNum
    Recovery Disk : $recDiskNum
    Recovery Part : $recPartNum
    Folder        : $RecoveryFolderName
    
    How restore.cmd finds the image:
      1. Scans ALL drive letters C: through Z:
      2. If not found, mounts Disk $recDiskNum Part $recPartNum as R:
      3. Uses whichever letter has the WIM
    
    Re-hide uses separate diskpart calls:
      Step 1: remove (letter)
      Step 2: set id=de94bba4-06d1-4d40-a16a-bfd50179d6ac
      Step 3: gpt attributes=0x8000000000000001
    "@ | Out-File (Join-Path $RecoveryPath "recovery_metadata.txt") -Encoding ASCII -Force
    
        Write-Log "Created: restore.cmd" "SUCCESS"
        Write-Log "Created: diagnose.cmd" "SUCCESS"
        Write-Log "Created: quick_restore.cmd" "SUCCESS"
    
        Write-Host ""
        Write-Host "  ┌──────────────────────────────────────────────────────────┐" -ForegroundColor Green
        Write-Host "  │  ✅ Recovery Scripts Created                              │" -ForegroundColor Green
        Write-Host "  │                                                            │" -ForegroundColor Green
        Write-Host "  │  Image search order:                                       │" -ForegroundColor Green
        Write-Host "  │  1. Scan ALL drive letters (C: through Z:)                │" -ForegroundColor Green
        Write-Host "  │  2. Diskpart mount Disk $recDiskNum Part $recPartNum as R:" -ForegroundColor Green
        Write-Host "  │  3. Use whichever letter has install.wim                  │" -ForegroundColor Green
        Write-Host "  │                                                            │" -ForegroundColor Green
        Write-Host "  │  Works whether partition is D:, R:, or hidden!            │" -ForegroundColor Green
        Write-Host "  └──────────────────────────────────────────────────────────┘" -ForegroundColor Green
    
        return $true
    }
    
    # ============================================================
    # STEP 3: CREATE DESKTOP SHORTCUTS
    # ============================================================
    
    function Invoke-Step3_CreateShortcuts {
        param(
            [string]$RecoveryDrive,
            [string]$RecoveryPath
        )
    
        Write-Log "STEP 3: CREATE DESKTOP SHORTCUTS" "STEP"
    
        $recPartition = Get-Partition -DriveLetter ($RecoveryDrive.TrimEnd(':'))
        $recPartNum   = $recPartition.PartitionNumber
        $recDiskNum   = $recPartition.DiskNumber
    
        $createShortcut = Read-Host "  Create desktop shortcuts? (Y/N) [Y]"
        if ([string]::IsNullOrEmpty($createShortcut)) { $createShortcut = 'Y' }
    
        if ($createShortcut -ne 'Y' -and $createShortcut -ne 'y') {
            Write-Log "Desktop shortcuts skipped." "INFO"
            return $true
        }
    
        # ── PowerShell Launcher ──
        $launcherPS = @"
    #Requires -RunAsAdministrator
    Add-Type -AssemblyName System.Windows.Forms
    
    `$msg = "WARNING: This will ERASE ALL DATA on C: and restore the factory image with all original apps and drivers.`n`nALL personal files will be DELETED.`n`nProceed?"
    `$r = [System.Windows.Forms.MessageBox]::Show(`$msg, "FACTORY RESET", "YesNo", "Warning")
    
    if (`$r -eq "Yes") {
        `$r2 = [System.Windows.Forms.MessageBox]::Show(
            "FINAL CONFIRMATION`n`nComputer will restart into recovery.`nEstimated time: 20-40 minutes.`n`nContinue?",
            "CONFIRM FACTORY RESET", "YesNo", "Stop")
    
        if (`$r2 -eq "Yes") {
            Write-Host "Preparing recovery environment..."
            reagentc /enable 2>`$null
            reagentc /boottore
            shutdown /r /t 5 /c "Restarting for Factory Reset..."
        }
    }
    "@
    
        # ── Batch Wrapper (auto-elevates) ──
        $batchWrapper = @"
    @echo off
    net session >nul 2>&1
    if %errorlevel% neq 0 (
        echo Requesting administrator privileges...
        powershell -Command "Start-Process cmd -ArgumentList '/c cd /d \"%~dp0\" && powershell -ExecutionPolicy Bypass -File \"%~dp0FactoryReset_Launcher.ps1\"' -Verb RunAs"
        exit /b
    )
    powershell -ExecutionPolicy Bypass -File "%~dp0FactoryReset_Launcher.ps1"
    "@
    
        # ── Instructions ──
        $instructions = @"
    FACTORY RESET INSTRUCTIONS
    ===========================
    
    AUTOMATIC (Windows boots normally):
      1. Double-click "Factory Reset.bat" on Desktop
      2. Confirm twice
      3. PC restarts into recovery mode
      4. In recovery CMD, mount R: and run restore:
         diskpart
         select disk $recDiskNum
         select partition $recPartNum
         assign letter=R
         exit
         R:\$RecoveryFolderName\restore.cmd
    
    MANUAL (SHIFT + Restart):
      1. Hold SHIFT + click Restart
      2. Troubleshoot → Command Prompt
      3. Run the diskpart + restore commands above
    
    USB BOOT (Windows won't start):
      1. Boot from Windows 11 USB
      2. Repair → Command Prompt
      3. Run the diskpart + restore commands above
    
    DIAGNOSTICS (safe test first):
      Mount R: then run: R:\$RecoveryFolderName\diagnose.cmd
    
    LOG FILES:
      C:\temp\restore_log.txt   (after restore)
      C:\temp\diagnose_log.txt  (after diagnostics)
    
    PARTITION INFO:
      Recovery: Disk $recDiskNum, Partition $recPartNum
    "@
    
        # ── Save files ──
        $desktop = "$env:USERPROFILE\Desktop"
    
        $launcherPS   | Out-File "$desktop\FactoryReset_Launcher.ps1" -Encoding UTF8 -Force
        $batchWrapper | Out-File "$desktop\Factory Reset.bat" -Encoding ASCII -Force
        $instructions | Out-File "$desktop\Factory Reset - Instructions.txt" -Encoding UTF8 -Force
        $instructions | Out-File (Join-Path $RecoveryPath "INSTRUCTIONS.txt") -Encoding UTF8 -Force
    
        Write-Host "    ✓ Factory Reset.bat" -ForegroundColor Green
        Write-Host "    ✓ FactoryReset_Launcher.ps1" -ForegroundColor Green
        Write-Host "    ✓ Factory Reset - Instructions.txt" -ForegroundColor Green
        Write-Host "    ✓ INSTRUCTIONS.txt (on recovery partition)" -ForegroundColor Green
    
        Write-Log "Desktop shortcuts created." "SUCCESS"
        return $true
    }
    
    # ============================================================
    # STEP 4: HIDE PARTITION (Optional)
    # ============================================================
    
    function Invoke-Step4_HidePartition {
        param([string]$RecoveryDrive)
    
        Write-Log "STEP 4: HIDE & PROTECT RECOVERY PARTITION" "STEP"
    
        $driveLetter = $RecoveryDrive.TrimEnd(':')
    
        # Find volume number
        $dpListFile = "$env:TEMP\dp_list.txt"
        "list volume" | Out-File -FilePath $dpListFile -Encoding ASCII
        $dpOutput = & diskpart /s $dpListFile 2>&1
        Remove-Item $dpListFile -Force -ErrorAction SilentlyContinue
    
        $volumeNumber = $null
        foreach ($line in $dpOutput) {
            if ($line -match "Volume\s+(\d+)\s+$driveLetter\s") {
                $volumeNumber = $Matches[1]
                break
            }
        }
    
        if ($null -eq $volumeNumber) {
            Write-Log "Could not find volume for ${driveLetter}:." "ERROR"
            return $false
        }
    
        Write-Host ""
        Write-Host "  This will:" -ForegroundColor Yellow
        Write-Host "    • Remove drive letter ${driveLetter}:" -ForegroundColor White
        Write-Host "    • Hide partition from File Explorer" -ForegroundColor White
        Write-Host "    • Set GPT recovery attributes" -ForegroundColor White
        Write-Host "    • Restore scripts will auto-mount it as R:" -ForegroundColor White
        Write-Host ""
        Write-Host "  Volume $volumeNumber = ${driveLetter}:" -ForegroundColor Cyan
        Write-Host ""
    
        $confirm = Read-Host "  Type 'HIDE' to proceed (anything else to cancel)"
    
        if ($confirm -eq 'HIDE') {
            # Separate operations for reliability
    
            # Remove letter
            $dp1 = @"
    select volume $volumeNumber
    remove letter=$driveLetter
    exit
    "@
            $dp1 | Out-File "$env:TEMP\dp_h1.txt" -Encoding ASCII
            & diskpart /s "$env:TEMP\dp_h1.txt"
            Remove-Item "$env:TEMP\dp_h1.txt" -Force -ErrorAction SilentlyContinue
            Start-Sleep -Seconds 2
    
            # Set partition type
            $dp2 = @"
    select volume $volumeNumber
    set id=de94bba4-06d1-4d40-a16a-bfd50179d6ac override
    exit
    "@
            $dp2 | Out-File "$env:TEMP\dp_h2.txt" -Encoding ASCII
            & diskpart /s "$env:TEMP\dp_h2.txt"
            Remove-Item "$env:TEMP\dp_h2.txt" -Force -ErrorAction SilentlyContinue
            Start-Sleep -Seconds 2
    
            # Set GPT attributes
            $dp3 = @"
    select volume $volumeNumber
    gpt attributes=0x8000000000000001
    exit
    "@
            $dp3 | Out-File "$env:TEMP\dp_h3.txt" -Encoding ASCII
            & diskpart /s "$env:TEMP\dp_h3.txt"
            Remove-Item "$env:TEMP\dp_h3.txt" -Force -ErrorAction SilentlyContinue
    
            Write-Host ""
            Write-Host "  ┌──────────────────────────────────────────────────┐" -ForegroundColor Green
            Write-Host "  │  ✅ Partition hidden and protected!               │" -ForegroundColor Green
            Write-Host "  │                                                    │" -ForegroundColor Green
            Write-Host "  │  To unhide later:                                 │" -ForegroundColor Green
            Write-Host "  │  diskpart → list volume → select volume X →       │" -ForegroundColor Green
            Write-Host "  │  assign letter=R                                  │" -ForegroundColor Green
            Write-Host "  └──────────────────────────────────────────────────┘" -ForegroundColor Green
    
            Write-Log "Partition hidden successfully." "SUCCESS"
            return $true
        }
    
        Write-Log "Hide cancelled by user." "WARNING"
        return $false
    }
    
    # ============================================================
    # MAIN
    # ============================================================
    
    function Main {
        "Recovery Setup Log (Final) - $(Get-Date)" | Out-File -FilePath $LogFile -Force
    
        Show-Banner
    
        # ── Admin check ──
        $identity  = [Security.Principal.WindowsIdentity]::GetCurrent()
        $principal = New-Object Security.Principal.WindowsPrincipal($identity)
        if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
            Write-Log "Must run as Administrator!" "ERROR"
            Write-Host "  Right-click PowerShell → Run as Administrator" -ForegroundColor Yellow
            Read-Host "  Press Enter to exit"
            exit 1
        }
    
        Write-Log "Running with Administrator privileges." "SUCCESS"
    
        # ── Select recovery drive ──
        if ([string]::IsNullOrEmpty($RecoveryDriveLetter)) {
            $RecoveryDriveLetter = Get-RecoveryDrive
            if ($null -eq $RecoveryDriveLetter) {
                Write-Log "No recovery partition selected. Exiting." "ERROR"
                Read-Host "  Press Enter to exit"
                exit 1
            }
        }
    
        Write-Log "Selected recovery drive: $RecoveryDriveLetter" "INFO"
    
        # ── Prerequisites ──
        if (-not (Test-Prerequisites -RecoveryDrive $RecoveryDriveLetter)) {
            Write-Log "Prerequisites FAILED. Fix issues above and re-run." "ERROR"
            Read-Host "  Press Enter to exit"
            exit 1
        }
    
        Write-Log "All prerequisites PASSED." "SUCCESS"
    
        # ── Confirm ──
        Write-Host ""
        Write-Host "  ┌──────────────────────────────────────────────────────────┐" -ForegroundColor Yellow
        Write-Host "  │  Process:                                                 │" -ForegroundColor Yellow
        Write-Host "  │                                                            │" -ForegroundColor Yellow
        Write-Host "  │  Step 1  : Capture WIM (VSS Shadow Copy)                  │" -ForegroundColor Yellow
        Write-Host "  │  Step 1.5: Generate unattend.xml (silent OOBE)            │" -ForegroundColor Yellow
        Write-Host "  │  Step 2  : Create restore scripts (WinRE-compatible)      │" -ForegroundColor Yellow
        Write-Host "  │  Step 3  : Desktop shortcuts                              │" -ForegroundColor Yellow
        Write-Host "  │  Step 4  : (Optional) Hide partition                      │" -ForegroundColor Yellow
        Write-Host "  │                                                            │" -ForegroundColor Yellow
        Write-Host "  │  ✅ All apps, drivers, settings preserved on restore      │" -ForegroundColor Yellow
        Write-Host "  │  ⏱  Estimated: 30-75 minutes                              │" -ForegroundColor Yellow
        Write-Host "  └──────────────────────────────────────────────────────────┘" -ForegroundColor Yellow
        Write-Host ""
    
        $proceed = Read-Host "  Proceed? (Y/N)"
        if ($proceed -ne 'Y' -and $proceed -ne 'y') {
            Write-Log "User cancelled." "WARNING"
            exit 0
        }
    
        $recoveryFolderPath = Join-Path $RecoveryDriveLetter $RecoveryFolderName
    
        # ═══════════════════════════════════
        #  STEP 1: Capture WIM
        # ═══════════════════════════════════
        $step1 = Invoke-Step1_CaptureWIM `
            -RecoveryDrive $RecoveryDriveLetter `
            -FolderName $RecoveryFolderName `
            -Name $ImageName `
            -Description $ImageDescription `
            -Compression $CompressionType
    
        if (-not $step1.Success) {
            Write-Log "Step 1 FAILED. Check DISM log." "ERROR"
            Write-Host "  notepad C:\Windows\Logs\DISM\dism.log" -ForegroundColor Yellow
            Read-Host "  Press Enter to exit"
            exit 1
        }
    
        # ═══════════════════════════════════
        #  STEP 1.5: Unattend.xml
        # ═══════════════════════════════════
        Write-Host ""
        $doUnattend = Read-Host "  Configure silent recovery (skip OOBE screens)? (Y/N) [Y]"
        if ([string]::IsNullOrEmpty($doUnattend)) { $doUnattend = 'Y' }
    
        if ($doUnattend -eq 'Y' -or $doUnattend -eq 'y') {
            $settings = Get-UnattendSettings
            Save-UnattendXml -RecoveryPath $recoveryFolderPath -Settings $settings
        }
        else {
            Write-Log "Unattend.xml skipped. OOBE screens will appear on restore." "INFO"
        }
    
        # ═══════════════════════════════════
        #  STEP 2: Recovery Scripts
        # ═══════════════════════════════════
        Invoke-Step2_CreateRecoveryScripts `
            -RecoveryDrive $RecoveryDriveLetter `
            -RecoveryPath $recoveryFolderPath `
            -WimFile $step1.WimFile
    
        # ═══════════════════════════════════
        #  STEP 3: Desktop Shortcuts
        # ═══════════════════════════════════
        Invoke-Step3_CreateShortcuts `
            -RecoveryDrive $RecoveryDriveLetter `
            -RecoveryPath $recoveryFolderPath
    
        # ═══════════════════════════════════
        #  STEP 4: Hide Partition
        # ═══════════════════════════════════
        Write-Host ""
        Write-Host "  ┌──────────────────────────────────────────────────────────┐" -ForegroundColor Cyan
        Write-Host "  │  Hide recovery partition?                                 │" -ForegroundColor Cyan
        Write-Host "  │                                                            │" -ForegroundColor Cyan
        Write-Host "  │  [Y] Production — hidden from Explorer, auto-mounted     │" -ForegroundColor Cyan
        Write-Host "  │  [N] Testing — keep visible for debugging                │" -ForegroundColor Cyan
        Write-Host "  └──────────────────────────────────────────────────────────┘" -ForegroundColor Cyan
        Write-Host ""
    
        $hide = Read-Host "  (Y/N)"
        if ($hide -eq 'Y' -or $hide -eq 'y') {
            Invoke-Step4_HidePartition -RecoveryDrive $RecoveryDriveLetter
        }
        else {
            Write-Log "Partition not hidden (testing mode)." "INFO"
        }
    
        # ═══════════════════════════════════
        #  COMPLETE
        # ═══════════════════════════════════
        Write-Host ""
        Write-Host "  ╔════════════════════════════════════════════════════════════╗" -ForegroundColor Green
        Write-Host "  ║  ✅  RECOVERY PARTITION SETUP COMPLETE!                    ║" -ForegroundColor Green
        Write-Host "  ╠════════════════════════════════════════════════════════════╣" -ForegroundColor Green
        Write-Host "  ║                                                            ║" -ForegroundColor Green
        Write-Host "  ║  Recovery Partition Contents:                               ║" -ForegroundColor Green
        Write-Host "  ║  ├── install.wim           (full system image)            ║" -ForegroundColor Green
        Write-Host "  ║  ├── unattend.xml          (silent OOBE)                  ║" -ForegroundColor Green
        Write-Host "  ║  ├── restore.cmd           (interactive restore + log)    ║" -ForegroundColor Green
        Write-Host "  ║  ├── diagnose.cmd          (safe testing)                 ║" -ForegroundColor Green
        Write-Host "  ║  ├── quick_restore.cmd     (automated)                    ║" -ForegroundColor Green
        Write-Host "  ║  ├── recovery_metadata.txt (partition info)               ║" -ForegroundColor Green
        Write-Host "  ║  └── INSTRUCTIONS.txt      (how to restore)              ║" -ForegroundColor Green
        Write-Host "  ║                                                            ║" -ForegroundColor Green
        Write-Host "  ║  Desktop Files:                                            ║" -ForegroundColor Green
        Write-Host "  ║  ├── Factory Reset.bat                                    ║" -ForegroundColor Green
        Write-Host "  ║  ├── FactoryReset_Launcher.ps1                            ║" -ForegroundColor Green
        Write-Host "  ║  └── Factory Reset - Instructions.txt                     ║" -ForegroundColor Green
        Write-Host "  ║                                                            ║" -ForegroundColor Green
        Write-Host "  ║  HOW TO RESTORE:                                           ║" -ForegroundColor Green
        Write-Host "  ║  1. SHIFT+Restart → Troubleshoot → CMD                    ║" -ForegroundColor Green
        Write-Host "  ║  2. Mount R: (diskpart commands in Instructions.txt)      ║" -ForegroundColor Green
        Write-Host "  ║  3. R:\$RecoveryFolderName\diagnose.cmd  (test first)"       -ForegroundColor Green
        Write-Host "  ║  4. R:\$RecoveryFolderName\restore.cmd   (restore)"          -ForegroundColor Green
        Write-Host "  ║                                                            ║" -ForegroundColor Green
        Write-Host "  ║  LOG FILES (after restore):                                ║" -ForegroundColor Green
        Write-Host "  ║  C:\temp\restore_log.txt                                  ║" -ForegroundColor Green
        Write-Host "  ║  C:\temp\diagnose_log.txt                                 ║" -ForegroundColor Green
        Write-Host "  ║                                                            ║" -ForegroundColor Green
        Write-Host "  ║  Setup log: $LogFile" -ForegroundColor Green
        Write-Host "  ║                                                            ║" -ForegroundColor Green
        Write-Host "  ╚════════════════════════════════════════════════════════════╝" -ForegroundColor Green
        Write-Host ""
    
        Write-Log "Setup completed successfully." "SUCCESS"
        Read-Host "  Press Enter to exit"
    }
    
    # ── Run ──
    Main
    

    Phase 3: How the Restore Scripts Work

    How to Perform a Factory Reset

    Method 1: From Windows (System Boots Normally)

    1. Hold SHIFT + click "Restart"
    2. Troubleshoot → Command Prompt
    3. Mount the recovery partition:
       diskpart
       select disk 0
       select partition 5        ← your recovery partition number
       assign letter=R
       exit
    4. Run: R:\RecoveryImage\restore.cmd
    

    Method 2: From USB (Windows Won’t Boot)

    1. Boot from Windows 11 installation USB
    2. Click "Repair your computer"
    3. Troubleshoot → Command Prompt
    4. Same diskpart + restore commands as above
    

    What restore.cmd Does

    Step 1: FIND the recovery image
      ├── Scans C: through Z: for RecoveryImage\install.wim
      ├── If found (e.g., on D:) → uses that drive
      └── If not found → diskpart mounts known partition as R:
    
    Step 2: FORMAT C: drive
      ├── format C: /FS:NTFS /Q /Y
      └── Fallback: diskpart format
    
    Step 3: APPLY image
      └── DISM.exe /Apply-Image /ImageFile:[drive]:\RecoveryImage\install.wim
                   /Index:1 /ApplyDir:C:\
    
    Step 4: REBUILD boot
      ├── Assign S: to EFI partition
      ├── bcdboot C:\Windows /s S: /f UEFI
      └── Remove S:
    
    Step 5: APPLY unattend.xml
      ├── Copy to C:\Windows\Panther\unattend\
      ├── Copy to C:\Windows\System32\Sysprep\
      └── Inject BypassNRO into offline registry
    
    Step 6: RE-HIDE recovery partition
      ├── Call 1: remove (drive letter)
      ├── Call 2: set id=de94bba4... (recovery type)
      └── Call 3: gpt attributes=0x80... (protected)
    
    Reboot → Windows starts → Silent OOBE → Desktop → Done!
    

    Log Files

    After a restore, check these logs:

    Log Location Purpose
    Restore log C:\temp\restore_log.txt Every step with timestamps
    DISM log C:\Windows\Logs\DISM\dism.log DISM operation details
    Setup log Desktop\RecoverySetup_Log.txt Initial script setup

    Phase 4: Testing & Validation

    Pre-Restore Test (Safe)

    1. Boot into WinRE (SHIFT + Restart)
    2. Command Prompt
    3. Mount recovery partition
    4. Run: R:\RecoveryImage\diagnose.cmd
       (Makes no changes — just verifies everything)
    

    Post-Restore Verification Checklist

    After restoring, verify:
    
    [ ] Windows boots successfully
    [ ] All applications are present (Start Menu)
    [ ] All drivers work (Device Manager — no yellow triangles)
    [ ] Network connectivity works
    [ ] User account was auto-created (if unattend configured)
    [ ] No OOBE setup screens appeared (if unattend configured)
    [ ] C:\temp\restore_log.txt shows all steps succeeded
    [ ] Recovery partition is hidden (not visible in Explorer)
    

    Lessons Learned & Pitfalls

    Pitfall 1: “ClientAccessible” Typo

    ❌ "ClientAccesible"  (one 's')  → VSS returns error 5
    ✅ "ClientAccessible" (two 's') → Works
    

    Pitfall 2: DISM Error 0x80070020

    ❌ DISM /Capture-Image /CaptureDir:C:\  → Files locked by running OS
    ✅ VSS Shadow Copy → Mount → DISM captures from snapshot
    

    Pitfall 3: “Reset this PC” Strips Apps

    ❌ reagentc /setosimage → "Reset this PC" → Apps removed
    ✅ DISM /Apply-Image → Byte-for-byte restore → Everything preserved
    

    Pitfall 4: DISM Error 87 in WinRE

    ❌ setlocal EnableDelayedExpansion + !var! → Empty in WinRE
    ✅ Hardcoded paths, no delayed expansion
    

    Pitfall 5: timeout Doesn’t Exist in WinRE

    ❌ timeout /t 3 /nobreak > nul → Command not found
    ✅ ping -n 4 127.0.0.1 > nul → 3-second delay
    

    Pitfall 6: WinRE Reassigns Drive Letters

    ❌ Assume R: = Recovery → WinRE assigns D: instead
    ✅ Scan ALL letters C-Z first, then diskpart fallback
    

    Pitfall 7: Combined DiskPart Re-hide Fails

    ❌ remove letter=R (fails if letter is D:) → set id skipped
    ✅ Separate calls: remove (no letter specified) → set id → gpt attributes
    

    Pitfall 8: Log Lost When C: Formatted

    ❌ Log on C: → format C: → Log gone
    ✅ Copy log to recovery drive before format, restore after
    

    FAQ

    Q: How much space does the recovery partition need? A: About 70% of your used space on C:. If you have 40 GB used, you need ~28 GB for the WIM file. Plan for 30-50 GB.

    Q: Can I update the recovery image later? A: Yes! Re-run the PowerShell script. When it asks about the existing WIM, choose “Y” to overwrite with a fresh capture.

    Q: Does this work with BitLocker? A: You’ll need to suspend BitLocker before capturing and ensure the recovery environment can access the drives. The restore scripts don’t handle BitLocker decryption.

    Q: Can I deploy this to multiple identical PCs? A: For multi-PC deployment, add Sysprep generalization before capture: C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /shutdown Then boot from WinPE and capture.

    Q: What if the recovery partition gets deleted? A: The WIM file is gone. Always keep a backup copy on an external drive. You can recreate the partition and re-run the script.

    Q: Why not use WinPE USB for capture instead of VSS? A: You can! Boot from WinPE USB, then run: DISM /Capture-Image /ImageFile:R:\RecoveryImage\install.wim /CaptureDir:C:\ /Name:Recovery /Compress:maximum No file locks from WinPE. VSS is the “no reboot needed” alternative.


    License

    This script is provided as-is for educational purposes. Test thoroughly before deploying in production environments. Always maintain backups before performing system modifications.


    How to Use This Blog Post

    1. Copy everything between the outer ` markdown ` and ` ` tags
    2. Paste into your blog editor (WordPress, Ghost, Hugo, Jekyll, etc.)
    3. The code blocks inside will render correctly since they use indented fences
    4. The complete PowerShell script is included inline — readers can copy-paste directly
    5. All diagrams use ASCII art — no external images needed
  • Driver Update script

    Driver update script thats called using Autounattended xml file

    Driver update from Networkshare

    Execution flow

    flowchart TB
        START([🚀 Script Start]) --> INIT[Initialize-PersistentScript<br/>Copy script to C:\Temp\]
        INIT -->|Failed| FAIL_INIT([❌ Throw Error])
        INIT -->|Success| TRANSCRIPT[Start-Transcript<br/>C:\Temp\DriverUpdateScript.log]
        TRANSCRIPT --> HEADER[Display Header<br/>User, Computer, Model, Config]
        HEADER --> ORPHAN[Stop Orphan Driver Processes<br/>drvinst, pnputil, DPInst, etc.]
        ORPHAN --> LOADSTATE[Get-ScriptState<br/>Load or create state JSON]
        LOADSTATE --> SHOWSTATE[Display Current State<br/>All properties]
        SHOWSTATE --> CHECKPHASE{state.Phase?}
    
        %% ============================================================
        %% PHASE 0
        %% ============================================================
        CHECKPHASE -->|Phase 0| USB_CHECK{USBCompleted?}
    
        subgraph PHASE0A ["🔌 PHASE-0A: USB Network Drivers"]
            USB_CHECK -->|No| USB_FIND[Scan drives D: - Z:<br/>Look for DriversOS / Drivers folder]
            USB_FIND -->|Not Found| USB_SKIP[No USB folder found<br/>Skip USB phase]
            USB_FIND -->|Found| USB_INSTALL[Install-DriversFromFolder<br/>SkipDeviceMatching = TRUE<br/>Install ALL drivers from USB]
            USB_INSTALL --> USB_EACH[For each INF file:<br/>Install-SingleDriver<br/>pnputil /add-driver /install]
            USB_EACH --> USB_RESULT{NeedsReboot?}
            USB_RESULT -->|Yes| USB_REBOOT[Schedule-RebootAndContinue<br/>Set RunOnce registry<br/>Reboot in 15 seconds]
            USB_REBOOT --> REBOOT_USB([🔄 REBOOT])
            REBOOT_USB -.->|After reboot| USB_CHECK
            USB_RESULT -->|No| USB_DONE[USBCompleted = true<br/>USBRebootDone = true]
            USB_SKIP --> USB_DONE
        end
    
        USB_CHECK -->|Yes, not rebooted| USB_FINALIZE[Post-reboot:<br/>USB drivers finalized]
        USB_FINALIZE --> NET_CHECK
        USB_CHECK -->|Yes, rebooted| NET_CHECK
        USB_DONE --> NET_CHECK
    
        subgraph PHASE0B ["📡 PHASE-0B: Network Share Drivers"]
            NET_CHECK{NetworkShare<br/>Completed?}
            NET_CHECK -->|No| GUEST_CHECK{GuestAuth<br/>Configured?}
    
            subgraph GUESTAUTH ["🔓 SMB Guest Access Fix"]
                GUEST_CHECK -->|No| GUEST_POLICY[Create policy key if missing<br/>HKLM:\SOFTWARE\Policies\<br/>Microsoft\Windows\LanmanWorkstation]
                GUEST_POLICY --> GUEST_INSECURE[AllowInsecureGuestAuth = 1<br/>Policy + Service level]
                GUEST_INSECURE --> GUEST_SIGN1[RequireSecuritySignature = 0<br/>Disable 'always sign']
                GUEST_SIGN1 --> GUEST_SIGN2[EnableSecuritySignature = 0<br/>Disable 'sign if server agrees']
                GUEST_SIGN2 --> GUEST_RESTART[Restart LanmanWorkstation<br/>+ dependent services]
                GUEST_RESTART --> GUEST_VERIFY[Verify: Read back all 4 values<br/>Display OK/FAIL table]
                GUEST_VERIFY --> SYSINFO
            end
    
            GUEST_CHECK -->|Yes| SYSINFO
    
            subgraph MODELPATH ["🏷️ Model Detection & Path Building"]
                SYSINFO[Get-SystemInfo<br/>Manufacturer + Model]
                SYSINFO --> CLEANMODEL["Clean model name<br/>'Latitude 5400' → 'latitude5400'"]
                CLEANMODEL --> MFGFOLDER["Map manufacturer → folder<br/>'Dell Inc.' → 'dell'"]
                MFGFOLDER --> BUILDPATH["Build UNC path<br/>\\networkshare\dell\driver\latitude5400"]
            end
    
            BUILDPATH --> TESTSHARE[Test-NetworkShareAccess<br/>10 retries × 10s delay]
            TESTSHARE -->|Accessible| DEVSCAN
            TESTSHARE -->|Failed| TRYALT["Try alternative names:<br/>latitude-5400<br/>latitude_5400<br/>latitude 5400"]
            TRYALT -->|Found| DEVSCAN
            TRYALT -->|All failed| NET_NOTFOUND[No share folder found<br/>Skip network share phase]
    
            subgraph DEVMATCH ["🔍 Device Matching"]
                DEVSCAN[Get-SystemDeviceIds<br/>WMI Win32_PnPEntity<br/>Fallback: Get-PnpDevice]
                DEVSCAN --> MATCHINFS["For each INF:<br/>Parse [Manufacturer] + [Models]<br/>Extract hardware IDs"]
                MATCHINFS --> COMPARE["Compare INF IDs vs System IDs<br/>HashSet O(1) lookup"]
                COMPARE --> MATCHRESULT["Display results:<br/>Matched / Skipped / Mode"]
            end
    
            MATCHRESULT --> NETINSTALL[Install-DriversFromFolder<br/>Only MATCHED drivers]
            NETINSTALL --> NET_EACH["For each matched INF:<br/>Install-SingleDriver<br/>pnputil /add-driver /install<br/>Timeout monitoring<br/>Spinner animation"]
            NET_EACH --> NET_RESULT{NeedsReboot?}
            NET_RESULT -->|Yes| NET_REBOOT[Schedule-RebootAndContinue]
            NET_REBOOT --> REBOOT_NET([🔄 REBOOT])
            REBOOT_NET -.->|After reboot| NET_CHECK
            NET_RESULT -->|No| NET_DONE[NetworkShareCompleted = true<br/>Phase = 1]
            NET_NOTFOUND --> NET_DONE
        end
    
        NET_CHECK -->|Yes, not rebooted| NET_FINALIZE[Post-reboot:<br/>Share drivers finalized<br/>Phase = 1]
        NET_CHECK -->|Yes, rebooted| PHASE1_START
        NET_FINALIZE --> PHASE1_START
        NET_DONE --> PHASE1_START
    
        %% ============================================================
        %% PHASE 1
        %% ============================================================
        CHECKPHASE -->|Phase ≥ 1| PHASE1_START
    
        subgraph PHASE1 ["🌐 PHASE-1: Online OEM Updates"]
            PHASE1_START[PHASE 1: ONLINE OEM UPDATES]
            PHASE1_START --> INET[Test-InternetConnectivity<br/>15 retries × 10s delay<br/>HEAD https://google.com]
            INET -->|No Internet| INET_FAIL([❌ Exit 1<br/>No internet])
            INET -->|Connected| CHOCO
    
            subgraph CHOCO_SETUP ["🍫 Chocolatey Setup"]
                CHOCO[Install-ChocolateyIfNeeded]
                CHOCO --> CHOCO_SRC["Set-ChocolateySource<br/>Add: chocosia → choco.local.xyz.com<br/>Disable: default community source"]
            end
    
            CHOCO_SRC --> MFG_DETECT[Get-SystemManufacturer<br/>Win32_ComputerSystem]
            MFG_DETECT --> MFG_CHECK{Manufacturer?}
    
            subgraph DELL ["🖥️ Dell Updates"]
                MFG_CHECK -->|Dell| DELL_CLI[Locate dcu-cli.exe<br/>or install via Chocolatey]
                DELL_CLI -->|Not found| DELL_CHOCO["Install-ChocoPackage<br/>'dellcommandupdate'<br/>Invoke-CommandVerbose"]
                DELL_CLI -->|Found| DELL_PHASE
                DELL_CHOCO --> DELL_PHASE
    
                DELL_PHASE{ManufacturerPhase?}
                DELL_PHASE -->|0| DELL_ADR["Dell Phase-0: ADR<br/>Configure ADR enable<br/>dcu-cli /driverinstall<br/>Verbose output streaming"]
                DELL_ADR --> DELL_ADR_EXIT{Exit code?}
                DELL_ADR_EXIT -->|0, 3| DELL_P2
                DELL_ADR_EXIT -->|1, 5| DELL_ADR_REBOOT[Reboot required]
                DELL_ADR_REBOOT --> REBOOT_DELL([🔄 REBOOT])
                REBOOT_DELL -.-> DELL_PHASE
                DELL_ADR_EXIT -->|2, other| DELL_ADR_FAIL[ADR failed<br/>Skip to Phase-2]
                DELL_ADR_FAIL --> DELL_P2
    
                DELL_PHASE -->|1| DELL_SCAN["Dell Phase-1: Scan<br/>dcu-cli /scan"]
                DELL_SCAN --> DELL_P2
    
                DELL_P2["Dell Phase-2: Apply Updates<br/>dcu-cli /applyupdates<br/>-forceUpdate=enable<br/>-autoSuspendBitLocker=enable<br/>Verbose: each driver shown"]
                DELL_PHASE -->|2| DELL_P2
                DELL_P2 --> DELL_P2_EXIT{Exit code?}
                DELL_P2_EXIT -->|0, 3| DELL_DONE[Dell updates complete]
                DELL_P2_EXIT -->|1, 5| DELL_P2_REBOOT[Reboot required]
                DELL_P2_REBOOT --> REBOOT_DELL2([🔄 REBOOT])
                REBOOT_DELL2 -.-> DELL_PHASE
                DELL_P2_EXIT -->|other| DELL_DONE
    
                DELL_PHASE -->|3+| DELL_DONE
            end
    
            subgraph HP ["🖨️ HP Updates"]
                MFG_CHECK -->|HP| HP_CLI[Locate HPImageAssistant.exe<br/>or install via Chocolatey]
                HP_CLI -->|Not found| HP_CHOCO["Install-ChocoPackage<br/>'hpimageassistant'<br/>Invoke-CommandVerbose"]
                HP_CLI -->|Found| HP_RUN
                HP_CHOCO --> HP_RUN
    
                HP_RUN["HP Image Assistant<br/>/Operation:Analyze<br/>/Action:Install<br/>/Selection:All<br/>Verbose output streaming"]
                HP_RUN --> HP_EXIT{Exit code?}
                HP_EXIT -->|0| HP_DONE[HP updates complete]
                HP_EXIT -->|256, 3010| HP_REBOOT_WARN[Reboot recommended]
                HP_EXIT -->|other| HP_CONTINUE[Continue anyway]
                HP_REBOOT_WARN --> HP_DONE
                HP_CONTINUE --> HP_DONE
            end
    
            MFG_CHECK -->|Other| MFG_UNSUPPORTED[Unsupported manufacturer<br/>Only Dell & HP handled]
        end
    
        %% ============================================================
        %% FINAL
        %% ============================================================
        DELL_DONE --> FINAL
        HP_DONE --> FINAL
        MFG_UNSUPPORTED --> FINAL
    
        subgraph CLEANUP ["✅ Final Cleanup"]
            FINAL[Stop remaining driver processes]
            FINAL --> SUMMARY["Display Final Summary<br/>System, Model, Share path<br/>Reboots, Added, Installed<br/>Skipped, Failed, Timed out<br/>Total time"]
            SUMMARY --> REMOVESTATE["Remove-ScriptState<br/>Delete state JSON<br/>Delete RunOnce registry<br/>Delete scheduled task"]
        end
    
        REMOVESTATE --> DONE([✅ Script Complete])
    
        %% ============================================================
        %% ERROR HANDLING
        %% ============================================================
        FAIL_INIT --> CATCH
        subgraph ERRORHANDLER ["❌ Error Handler"]
            CATCH[Catch block<br/>Display error message + line<br/>Display stack trace]
            CATCH --> EMERGENCY[Emergency cleanup<br/>Stop all driver processes<br/>ErrorAction = SilentlyContinue]
            EMERGENCY --> KEEPSTATE[State file kept<br/>Re-run script to continue]
        end
        KEEPSTATE --> FINALLY
    
        DONE --> FINALLY
        FINALLY[Finally block<br/>Stop-Transcript]
    
        %% ============================================================
        %% INSTALL-SINGLEDRIVER DETAIL
        %% ============================================================
        subgraph SINGLEDRIVER ["⚙️ Install-SingleDriver (called per INF)"]
            direction TB
            SD_START[Write driver header<br/>Progress bar + name] --> SD_CLEAN[Clear-DriverInstallEnvironment<br/>Kill leftover processes]
            SD_CLEAN --> SD_WRAPPER["Create wrapper .ps1<br/>Runs pnputil in child process"]
            SD_WRAPPER --> SD_SPAWN["Start-Process powershell<br/>-WindowStyle Hidden"]
            SD_SPAWN --> SD_MONITOR["Monitor loop:<br/>Spinner animation<br/>Elapsed/Remaining timer<br/>Check timeout"]
            SD_MONITOR --> SD_TIMEOUT{Timed out?}
            SD_TIMEOUT -->|Yes| SD_KILL["Kill process<br/>Stop-DriverInstallProcesses<br/>Return TimedOut result"]
            SD_TIMEOUT -->|No| SD_READ["Read output file<br/>Read exit code file"]
            SD_READ --> SD_PARSE["Parse pnputil output:<br/>added / installed /<br/>already exists / reboot"]
            SD_PARSE --> SD_EXIT["Evaluate exit code:<br/>0=OK, 1=warn, 259=staged<br/>3010=reboot, 482=partial"]
            SD_EXIT --> SD_RETURN[Return result object]
        end
    
        %% ============================================================
        %% INVOKE-COMMANDVERBOSE DETAIL
        %% ============================================================
        subgraph VERBOSE ["📺 Invoke-CommandVerbose (used by Dell/HP/Choco)"]
            direction TB
            V_START[Start process with<br/>RedirectStdout + Stderr] --> V_LOOP["Async ReadLineAsync loop:<br/>stdout → White text<br/>stderr → Yellow text<br/>Each line timestamped"]
            V_LOOP --> V_HEARTBEAT["If silent >30s:<br/>Print heartbeat message<br/>'...still running (120s)'"]
            V_HEARTBEAT --> V_DONE["Process exits<br/>Save full log to C:\Temp\<br/>Display duration + exit code"]
        end
    
        %% ============================================================
        %% STYLING
        %% ============================================================
        classDef phase fill:#c9f1f5,stroke:#e94560,color:#fff,stroke-width:2px
        classDef success fill:#0bd979,stroke:#00ff88,color:#fff,stroke-width:2px
        classDef reboot fill:#533483,stroke:#ffcc00,color:#fff,stroke-width:2px
        classDef error fill:#4a0000,stroke:#ff0000,color:#fff,stroke-width:2px
        classDef decision fill:#26948b,stroke:#0f3460,color:#fff,stroke-width:1px
        classDef process fill:#c9f1f5,stroke:#e2e2e2,color:#fff,stroke-width:1px
    
        class START,DONE success
        class REBOOT_USB,REBOOT_NET,REBOOT_DELL,REBOOT_DELL2 reboot
        class FAIL_INIT,INET_FAIL,CATCH error
        class CHECKPHASE,USB_CHECK,NET_CHECK,GUEST_CHECK,MFG_CHECK,DELL_PHASE,USB_RESULT,NET_RESULT,DELL_ADR_EXIT,DELL_P2_EXIT,HP_EXIT,SD_TIMEOUT decision
    

    Reboot Flow

    sequenceDiagram
        participant S as Script
        participant R as Registry RunOnce
        participant W as Windows
    
        S->>S: Save state to JSON
        S->>R: Write RunOnce entry (64-bit)
        alt RunOnce failed
            S->>W: Create Scheduled Task (fallback)
        end
        S->>W: Restart-Computer -Force
        W->>W: Reboot
        W->>R: Read RunOnce entry
        R->>S: Launch script automatically
        S->>S: Load state from JSON
        S->>S: Resume from saved Phase
    

    Network Share Folder Structure

    graph LR
        SHARE["\\networkshare"] --> DELL[dell/]
        SHARE --> HP[hp/]
        DELL --> DDRIVER[driver/]
        HP --> HDRIVER[driver/]
        DDRIVER --> LAT5400[latitude5400/]
        DDRIVER --> PREC3500[precision3500/]
        DDRIVER --> OPT7080[optiplex7080/]
        HDRIVER --> ELITE840[elitebook840g7/]
        HDRIVER --> PRO400[prodesk400g6/]
        LAT5400 --> AUDIO[Audio/]
        LAT5400 --> VIDEO[Video/]
        LAT5400 --> CHIP[Chipset/]
        AUDIO --> INF1[realtek.inf]
        VIDEO --> INF2[igdlh64.inf]
        CHIP --> INF3[SetupChipset.inf]
    
        style SHARE fill:#e94560,color:#fff
        style DELL fill:#0f3460,color:#fff
        style HP fill:#0f3460,color:#fff
        style LAT5400 fill:#533483,color:#fff
    

    USB Stick Structure (Minimal)

    graph LR
        USB["USB Drive E:\"] --> DOS[DriversOS/]
        DOS --> INTEL_LAN["Intel-I219LM/"]
        DOS --> INTEL_WIFI["Intel-AX211/"]
        DOS --> REALTEK["Realtek-RTL8111/"]
        INTEL_LAN --> INF1["e1d68x64.inf"]
        INTEL_WIFI --> INF2["netwtw10.inf"]
        REALTEK --> INF3["rt640x64.inf"]
    
        style USB fill:#e94560,color:#fff
        style DOS fill:#533483,color:#fff
    

    Powershell Script That searches the network share for drivers

    #Requires -RunAsAdministrator
    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'
    
    #===================================================================
    #  CONFIGURATION
    #===================================================================
    $Script:TempFolder       = "C:\Temp"
    $Script:PersistentScript = "C:\Temp\DriverUpdateScript.ps1"
    $Script:StateFile        = "C:\Temp\DriverUpdateState.json"
    $Script:LogFile          = "C:\Temp\DriverUpdateScript.log"
    $Script:RunOnceKey       = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
    $Script:RunOnceName      = 'DriverUpdateScript_RunOnce'
    $Script:DriverTimeout    = 300
    $Script:CooldownSeconds  = 5
    $Script:DriverProcesses  = @('drvinst','pnputil','DPInst','DPInst64','SetupAPI')
    $Script:SpinnerFrames    = @('[ ]','[= ]','[== ]','[=== ]','[====]','[ ===]','[ ==]','[ =]')
    $Script:ChocoSourceName  = 'chocosia'
    $Script:ChocoSourceUrl   = 'http://choco.local.iut-troyes.univ-reims.fr/repository/chocolatey-group'
    $Script:NetworkShareBase    = '\\synosia.local.iut-troyes.univ-reims.fr\batchs'
    $Script:ShareDriverFolder   = 'driver'
    $Script:ManufacturerShareMap = @{
        'Dell'             = 'dell'
        'Dell Inc.'        = 'dell'
        'HP'               = 'hp'
        'Hewlett-Packard'  = 'hp'
    }
    $Script:USBDriverFolders = @('DriversOS','Drivers')
    
    #===================================================================
    #  LOGGING HELPERS
    #===================================================================
    function Append-ToLog {
        param([Parameter(Mandatory=$false)][AllowEmptyString()][string]$Message = "")
        # No-op: Start-Transcript captures all console output automatically
    }
    
    function Write-Status {
        param(
            [Parameter(Mandatory)]
            [ValidateSet('ERROR','WARN','INFO','STEP','OK','PROGRESS','DEBUG','DRIVER','TIME','SKIP','KILL')]
            [string]$Level,
            [Parameter(Mandatory)][string]$Message
        )
        $timestamp = Get-Date -Format "HH:mm:ss"
        $color = switch ($Level.ToUpper()) {
            'ERROR'    { 'Red' }
            'WARN'     { 'Yellow' }
            'INFO'     { 'Cyan' }
            'STEP'     { 'Magenta' }
            'OK'       { 'Green' }
            'PROGRESS' { 'White' }
            'DEBUG'    { 'DarkGray' }
            'DRIVER'   { 'Blue' }
            'TIME'     { 'DarkYellow' }
            'SKIP'     { 'DarkMagenta' }
            'KILL'     { 'Red' }
            default    { 'White' }
        }
        $prefix = switch ($Level.ToUpper()) {
            'ERROR'    { '[X]' }
            'WARN'     { '[!]' }
            'INFO'     { '[i]' }
            'STEP'     { '[>]' }
            'OK'       { '[+]' }
            'PROGRESS' { '[o]' }
            'DEBUG'    { '[.]' }
            'DRIVER'   { '[D]' }
            'TIME'     { '[T]' }
            'SKIP'     { '[S]' }
            'KILL'     { '[K]' }
            default    { '[ ]' }
        }
        $line = "[$timestamp] $prefix [$Level] $Message"
        Write-Host $line -ForegroundColor $color
    }
    
    function Write-Banner {
        param([string]$Text)
        $sep = "=" * 70
        Write-Host ""
        Write-Host $sep -ForegroundColor Magenta
        Write-Host "  $Text" -ForegroundColor Magenta
        Write-Host $sep -ForegroundColor Magenta
        Write-Host ""
    }
    
    function Write-SubBanner {
        param([string]$Text)
        Write-Host ""
        Write-Host "--- $Text ---" -ForegroundColor Yellow
        Write-Host ""
    }
    
    function Write-LoggedHost {
        param(
            [Parameter(Mandatory=$false)][AllowEmptyString()][string]$Message = "",
            [string]$ForegroundColor = 'White'
        )
        if ($Message) {
            Write-Host $Message -ForegroundColor $ForegroundColor
        }
        else {
            Write-Host ""
        }
    }
    
    function Format-Duration {
        param([double]$Seconds)
        if ($Seconds -lt 60) { return "{0:N1}s" -f $Seconds }
        elseif ($Seconds -lt 3600) {
            $m = [math]::Floor($Seconds / 60); $s = $Seconds % 60
            return "{0}m {1:N0}s" -f $m, $s
        }
        else {
            $h = [math]::Floor($Seconds / 3600); $m = [math]::Floor(($Seconds % 3600) / 60)
            return "{0}h {1}m" -f $h, $m
        }
    }
    
    function Get-ConsoleWidth {
        $w = 100
        try { $w = [Console]::WindowWidth - 5; if ($w -lt 50) { $w = 100 } } catch { }
        return $w
    }
    
    function Write-DriverHeader {
        param([string]$DriverName, [int]$Current, [int]$Total)
        if ($Total -le 0) { $Total = 1 }
        $pct = [math]::Round(($Current / $Total) * 100)
        $filled = [math]::Floor($pct / 5)
        $empty = 20 - $filled
        $bar = "[" + ("#" * $filled) + ("-" * $empty) + "]"
        $lines = @(
            "  ======================================================================"
            "  $bar $pct% ($Current of $Total drivers)"
            "  Driver: $DriverName"
            "  Timeout: $Script:DriverTimeout seconds"
            "  ======================================================================"
        )
        Write-Host ""
        foreach ($l in $lines) {
            Write-Host $l -ForegroundColor DarkCyan
        }
    }
    
    #===================================================================
    #  INITIALISATION
    #===================================================================
    function Initialize-PersistentScript {
        if (-not (Test-Path $Script:TempFolder)) {
            New-Item -Path $Script:TempFolder -ItemType Directory -Force | Out-Null
        }
        $sessionHeader = @(
            ""
            "=================================================================="
            "  NEW SESSION: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
            "  PID: $PID | User: $env:USERNAME | Computer: $env:COMPUTERNAME"
            "=================================================================="
            ""
        )
        foreach ($line in $sessionHeader) {
            Write-Host $line -ForegroundColor DarkGray
        }
        $currentScript = $MyInvocation.ScriptName
        if (-not $currentScript) { $currentScript = $PSCommandPath }
        if ($currentScript -and $currentScript -ne $Script:PersistentScript) {
            if (Test-Path $currentScript) {
                Write-LoggedHost "[INIT] Copying script to $Script:PersistentScript" -ForegroundColor Yellow
                Copy-Item -Path $currentScript -Destination $Script:PersistentScript -Force
                Write-LoggedHost "[INIT] Script copied successfully." -ForegroundColor Green
            }
        }
        if (-not (Test-Path $Script:PersistentScript)) {
            Write-LoggedHost "[ERROR] Persistent script could not be created." -ForegroundColor Red
            return $false
        }
        return $true
    }
    
    #===================================================================
    #  PROCESS-HANDLING HELPERS
    #===================================================================
    function Get-DriverInstallProcesses {
        [System.Collections.ArrayList]$collector = @()
        foreach ($name in $Script:DriverProcesses) {
            $found = $null
            try { $found = Get-Process -Name $name -ErrorAction SilentlyContinue } catch { }
            if ($null -ne $found) {
                foreach ($p in @($found)) { [void]$collector.Add($p) }
            }
        }
        $arr = [object[]]$collector.ToArray()
        return ,$arr
    }
    
    function Stop-DriverInstallProcesses {
        param([switch]$Silent)
        $killed = 0
        foreach ($name in $Script:DriverProcesses) {
            $found = $null
            try { $found = Get-Process -Name $name -ErrorAction SilentlyContinue } catch { }
            if ($null -ne $found) {
                foreach ($proc in @($found)) {
                    if (-not $Silent) {
                        Write-Status KILL "Terminating: $($proc.ProcessName) (PID $($proc.Id))"
                    }
                    try { $proc.CloseMainWindow() | Out-Null } catch { }
                    Start-Sleep -Milliseconds 300
                    try {
                        if (-not $proc.HasExited) { Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue }
                    } catch { }
                    $killed++
                }
            }
        }
        foreach ($name in $Script:DriverProcesses) {
            try {
                $saveEA = $ErrorActionPreference
                $ErrorActionPreference = 'SilentlyContinue'
                $null = cmd.exe /c "taskkill /F /IM `"$name.exe`" >nul 2>&1"
                $ErrorActionPreference = $saveEA
            }
            catch { try { $ErrorActionPreference = $saveEA } catch { } }
        }
        return $killed
    }
    
    function Wait-ForDriverProcesses {
        param([int]$TimeoutSeconds = 30)
        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        while ($sw.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
            $procs = Get-DriverInstallProcesses
            $procCount = ($procs | Measure-Object).Count
            if ($procCount -eq 0) { return $true }
            Start-Sleep -Milliseconds 500
        }
        return $false
    }
    
    function Clear-DriverInstallEnvironment {
        $procs = Get-DriverInstallProcesses
        $procCount = ($procs | Measure-Object).Count
        if ($procCount -gt 0) {
            Write-Status WARN "Found $procCount leftover driver process(es) - cleaning up..."
            $k = Stop-DriverInstallProcesses
            Write-Status INFO "Terminated $k process(es)"
            Start-Sleep -Seconds 2
        }
    }
    
    #===================================================================
    #  EXECUTABLE FINDER (validates file, not directory)
    #===================================================================
    function Find-Executable {
        param(
            [Parameter(Mandatory)][string]$FileName,
            [Parameter(Mandatory)][string[]]$KnownPaths,
            [string[]]$SearchRoots = @(),
            [string]$Label = "executable"
        )
    
        # 1 - Check known paths (MUST be a file, not a directory)
        foreach ($p in $KnownPaths) {
            if (Test-Path $p -PathType Leaf) {
                Write-Status OK "Found $Label at known path: $p"
                return $p
            }
        }
        Write-Status INFO "$Label not found at known paths."
    
        # 2 - Search Chocolatey lib folder
        $chocoLib = "$env:ProgramData\chocolatey\lib"
        if (Test-Path $chocoLib) {
            Write-Status INFO "Searching Chocolatey lib for $FileName..."
            $found = Get-ChildItem -Path $chocoLib -Filter $FileName -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1
            if ($found) {
                Write-Status OK "Found $Label in Chocolatey: $($found.FullName)"
                return $found.FullName
            }
        }
    
        # 3 - Search Chocolatey bin for shims
        $chocoBin = "$env:ProgramData\chocolatey\bin"
        if (Test-Path $chocoBin) {
            $shimPath = Join-Path $chocoBin $FileName
            if (Test-Path $shimPath -PathType Leaf) {
                Write-Status OK "Found $Label shim: $shimPath"
                return $shimPath
            }
        }
    
        # 4 - Search provided root directories
        $defaultRoots = @(
            'C:\Program Files'
            'C:\Program Files (x86)'
            $env:ProgramData
            'C:\HP'
            'C:\SWSetup'
            'C:\Dell'
        )
        $allRoots = @($SearchRoots) + $defaultRoots | Select-Object -Unique
        foreach ($root in $allRoots) {
            if (-not (Test-Path $root)) { continue }
            Write-Status DEBUG "Searching $root for $FileName..."
            $found = Get-ChildItem -Path $root -Filter $FileName -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1
            if ($found) {
                Write-Status OK "Found $Label via search: $($found.FullName)"
                return $found.FullName
            }
        }
    
        Write-Status WARN "$Label ($FileName) not found anywhere."
        return $null
    }
    
    #===================================================================
    #  VERBOSE COMMAND EXECUTION
    #===================================================================
    function Invoke-CommandVerbose {
        param(
            [Parameter(Mandatory)][string]$FilePath,
            [Parameter(Mandatory)][string]$Arguments,
            [Parameter(Mandatory)][string]$Label
        )
    
        # Validate: must exist AND be a file (not a directory)
        if (-not (Test-Path $FilePath -PathType Leaf)) {
            Write-Status ERROR "Executable not found or is a directory: $FilePath"
            return 999
        }
    
        $exeName = Split-Path $FilePath -Leaf
        Write-Status INFO "Executing: $exeName $Arguments"
        Write-Status DEBUG "Full path: $FilePath"
    
        $psi = [System.Diagnostics.ProcessStartInfo]::new()
        $psi.FileName = $FilePath
        $psi.Arguments = $Arguments
        $psi.UseShellExecute = $false
        $psi.RedirectStandardOutput = $true
        $psi.RedirectStandardError = $true
        $psi.CreateNoWindow = $true
        $process = [System.Diagnostics.Process]::new()
        $process.StartInfo = $psi
        try { $null = $process.Start() }
        catch {
            Write-Status ERROR "Failed to start $exeName : $($_.Exception.Message)"
            return 999
        }
        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        $lastActivity = [System.Diagnostics.Stopwatch]::StartNew()
        $heartbeatSec = 30
        $logLines = [System.Collections.ArrayList]::new()
        $stdoutTask = $process.StandardOutput.ReadLineAsync()
        $stderrTask = $process.StandardError.ReadLineAsync()
        $stdoutDone = $false
        $stderrDone = $false
        while ((-not $stdoutDone) -or (-not $stderrDone)) {
            $activity = $false
            if ((-not $stdoutDone) -and $stdoutTask.IsCompleted) {
                $line = $null
                try { $line = $stdoutTask.Result } catch { $stdoutDone = $true; continue }
                if ($null -ne $line) {
                    $trimmed = $line.Trim()
                    if ($trimmed) {
                        $ts = Get-Date -Format 'HH:mm:ss'
                        $formatted = "  [$ts] [$Label] $trimmed"
                        Write-Host $formatted -ForegroundColor White
                        [void]$logLines.Add("[$ts] $trimmed")
                    }
                    $stdoutTask = $process.StandardOutput.ReadLineAsync()
                    $activity = $true; $lastActivity.Restart()
                }
                else { $stdoutDone = $true }
            }
            if ((-not $stderrDone) -and $stderrTask.IsCompleted) {
                $line = $null
                try { $line = $stderrTask.Result } catch { $stderrDone = $true; continue }
                if ($null -ne $line) {
                    $trimmed = $line.Trim()
                    if ($trimmed) {
                        $ts = Get-Date -Format 'HH:mm:ss'
                        $formatted = "  [$ts] [$Label] [STDERR] $trimmed"
                        Write-Host $formatted -ForegroundColor Yellow
                        [void]$logLines.Add("[$ts] [STDERR] $trimmed")
                    }
                    $stderrTask = $process.StandardError.ReadLineAsync()
                    $activity = $true; $lastActivity.Restart()
                }
                else { $stderrDone = $true }
            }
            if ((-not $activity) -and ($lastActivity.Elapsed.TotalSeconds -ge $heartbeatSec)) {
                $ts = Get-Date -Format 'HH:mm:ss'
                $elapsed = [math]::Round($sw.Elapsed.TotalSeconds)
                $heartbeatMsg = "  [$ts] [$Label] ... still running (${elapsed}s elapsed)"
                Write-Host $heartbeatMsg -ForegroundColor DarkGray
                [void]$logLines.Add("[$ts] [heartbeat] ${elapsed}s elapsed")
                $lastActivity.Restart()
            }
            if (-not $activity) { Start-Sleep -Milliseconds 100 }
        }
        $process.WaitForExit()
        $sw.Stop()
        $exitCode = $process.ExitCode
        $safeLabel = $Label -replace '[^a-zA-Z0-9]', '_'
        $cmdLogFile = Join-Path $Script:TempFolder "${safeLabel}_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
        try {
            $header = "Command : $FilePath $Arguments`nExit    : $exitCode`nDuration: $(Format-Duration $sw.Elapsed.TotalSeconds)`n$('=' * 60)`n"
            $body = ($logLines | ForEach-Object { $_ }) -join "`n"
            "$header$body" | Out-File $cmdLogFile -Encoding UTF8 -Force
        } catch { }
        Write-Status TIME "Completed in $(Format-Duration $sw.Elapsed.TotalSeconds)"
        Write-Status INFO "Exit code: $exitCode"
        Write-Status DEBUG "Per-command log: $cmdLogFile"
        try { $process.Dispose() } catch { }
        return $exitCode
    }
    
    function Get-ChocoExePath {
        $cmd = Get-Command choco -ErrorAction SilentlyContinue
        if ($null -ne $cmd) { return $cmd.Source }
        $default = "$env:ProgramData\chocolatey\bin\choco.exe"
        if (Test-Path $default) { return $default }
        return $null
    }
    
    #===================================================================
    #  SMB GUEST AUTH & SIGNING FIX
    #===================================================================
    function Enable-GuestNetworkAccess {
        Write-SubBanner "Configuring SMB Guest Access & Signing"
        Write-Status INFO "Windows 11 blocks guest SMB access by default."
        Write-Status INFO "Applying registry fixes..."
        $policyPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\LanmanWorkstation'
        $servicePath = 'HKLM:\SYSTEM\CurrentControlSet\Services\LanmanWorkstation\Parameters'
        $changes = 0
        if (-not (Test-Path $policyPath)) {
            Write-Status INFO "Creating policy key: $policyPath"
            try {
                New-Item -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows' -Name 'LanmanWorkstation' -Force | Out-Null
                Write-Status OK "Policy key created."
                $changes++
            }
            catch { Write-Status ERROR "Failed to create policy key: $($_.Exception.Message)" }
        }
        else { Write-Status OK "Policy key exists." }
        try {
            $currentVal = $null
            try { $currentVal = Get-ItemPropertyValue -Path $policyPath -Name 'AllowInsecureGuestAuth' -ErrorAction SilentlyContinue } catch { }
            if ($currentVal -eq 1) { Write-Status OK "AllowInsecureGuestAuth (policy) already 1." }
            else {
                Set-ItemProperty -Path $policyPath -Name 'AllowInsecureGuestAuth' -Value 1 -Type DWord -Force
                Write-Status OK "AllowInsecureGuestAuth (policy) set to 1."
                $changes++
            }
        }
        catch { Write-Status ERROR "Failed: $($_.Exception.Message)" }
        try {
            $currentVal = $null
            try { $currentVal = Get-ItemPropertyValue -Path $servicePath -Name 'AllowInsecureGuestAuth' -ErrorAction SilentlyContinue } catch { }
            if ($currentVal -eq 1) { Write-Status OK "AllowInsecureGuestAuth (service) already 1." }
            else {
                Set-ItemProperty -Path $servicePath -Name 'AllowInsecureGuestAuth' -Value 1 -Type DWord -Force
                Write-Status OK "AllowInsecureGuestAuth (service) set to 1."
                $changes++
            }
        }
        catch { Write-Status ERROR "Failed: $($_.Exception.Message)" }
        try {
            $currentVal = $null
            try { $currentVal = Get-ItemPropertyValue -Path $servicePath -Name 'RequireSecuritySignature' -ErrorAction SilentlyContinue } catch { }
            if ($currentVal -eq 0) { Write-Status OK "RequireSecuritySignature already 0." }
            else {
                Set-ItemProperty -Path $servicePath -Name 'RequireSecuritySignature' -Value 0 -Type DWord -Force
                Write-Status OK "RequireSecuritySignature set to 0."
                $changes++
            }
        }
        catch { Write-Status ERROR "Failed: $($_.Exception.Message)" }
        try {
            $currentVal = $null
            try { $currentVal = Get-ItemPropertyValue -Path $servicePath -Name 'EnableSecuritySignature' -ErrorAction SilentlyContinue } catch { }
            if ($currentVal -eq 0) { Write-Status OK "EnableSecuritySignature already 0." }
            else {
                Set-ItemProperty -Path $servicePath -Name 'EnableSecuritySignature' -Value 0 -Type DWord -Force
                Write-Status OK "EnableSecuritySignature set to 0."
                $changes++
            }
        }
        catch { Write-Status ERROR "Failed: $($_.Exception.Message)" }
        if ($changes -gt 0) {
            Write-Status INFO "Restarting LanmanWorkstation service ($changes changes)..."
            try {
                $saveEA = $ErrorActionPreference
                $ErrorActionPreference = 'SilentlyContinue'
                $null = cmd.exe /c "net stop LanmanWorkstation /y >nul 2>&1"
                Start-Sleep -Seconds 2
                $null = cmd.exe /c "net start LanmanWorkstation >nul 2>&1"
                $null = cmd.exe /c "net start LanmanServer >nul 2>&1"
                $null = cmd.exe /c "net start Netlogon >nul 2>&1"
                $ErrorActionPreference = $saveEA
                Start-Sleep -Seconds 3
                Write-Status OK "LanmanWorkstation restarted."
            }
            catch {
                try { $ErrorActionPreference = $saveEA } catch { }
                Write-Status WARN "Service restart may need reboot."
            }
        }
        else { Write-Status OK "No changes needed." }
        $readBack = @(
            @{ Name = 'AllowInsecureGuestAuth (policy)';  Path = $policyPath;  Key = 'AllowInsecureGuestAuth'; Expected = 1 }
            @{ Name = 'AllowInsecureGuestAuth (service)'; Path = $servicePath; Key = 'AllowInsecureGuestAuth'; Expected = 1 }
            @{ Name = 'RequireSecuritySignature';         Path = $servicePath; Key = 'RequireSecuritySignature'; Expected = 0 }
            @{ Name = 'EnableSecuritySignature';          Path = $servicePath; Key = 'EnableSecuritySignature'; Expected = 0 }
        )
        Write-LoggedHost
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost "              SMB GUEST ACCESS VERIFICATION" -ForegroundColor Cyan
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        foreach ($item in $readBack) {
            $val = $null
            try { $val = Get-ItemPropertyValue -Path $item.Path -Name $item.Key -ErrorAction SilentlyContinue } catch { }
            $valStr = if ($null -ne $val) { $val.ToString() } else { 'NOT SET' }
            $match = ($val -eq $item.Expected)
            $statusColor = if ($match) { 'Green' } else { 'Red' }
            $statusIcon = if ($match) { 'OK' } else { 'FAIL' }
            Write-LoggedHost "  [$statusIcon] $($item.Name): $valStr (expected: $($item.Expected))" -ForegroundColor $statusColor
        }
        Write-LoggedHost "  Changes applied: $changes" -ForegroundColor White
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost
        return ($changes -ge 0)
    }
    
    #===================================================================
    #  SYSTEM MODEL & NETWORK SHARE PATH HELPERS
    #===================================================================
    function Get-SystemInfo {
        $cs = Get-CimInstance Win32_ComputerSystem
        $rawManufacturer = $cs.Manufacturer.Trim()
        $rawModel = $cs.Model.Trim()
        $cleanModel = ($rawModel -replace '\s', '').ToLower()
        return [PSCustomObject]@{
            RawManufacturer = $rawManufacturer
            RawModel        = $rawModel
            CleanModel      = $cleanModel
        }
    }
    
    function Get-ManufacturerShareFolder {
        param([Parameter(Mandatory)][string]$Manufacturer)
        foreach ($key in $Script:ManufacturerShareMap.Keys) {
            if ($Manufacturer -like "*$key*") { return $Script:ManufacturerShareMap[$key] }
        }
        return ($Manufacturer -replace '\s', '').ToLower()
    }
    
    function Build-NetworkDriverPath {
        param([Parameter(Mandatory)][string]$ManufacturerFolder, [Parameter(Mandatory)][string]$CleanModel)
        $path = Join-Path $Script:NetworkShareBase $ManufacturerFolder
        $path = Join-Path $path $Script:ShareDriverFolder
        $path = Join-Path $path $CleanModel
        return $path
    }
    
    function Test-NetworkShareAccess {
        param([Parameter(Mandatory)][string]$SharePath, [int]$MaxRetries = 10, [int]$RetryDelaySec = 10)
        Write-Status INFO "Testing access to: $SharePath"
        for ($i = 1; $i -le $MaxRetries; $i++) {
            try {
                if (Test-Path $SharePath -ErrorAction Stop) {
                    Write-Status OK "Network share accessible: $SharePath"
                    return $true
                }
            }
            catch { }
            if ($i -lt $MaxRetries) {
                Write-Status WARN "Attempt $i/$MaxRetries failed - waiting ${RetryDelaySec}s..."
                Start-Sleep -Seconds $RetryDelaySec
            }
        }
        Write-Status ERROR "Share not accessible after $MaxRetries attempts: $SharePath"
        return $false
    }
    
    #===================================================================
    #  DEVICE SCANNING & INF MATCHING
    #===================================================================
    function Get-SystemDeviceIds {
        $ids = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        $deviceCount = 0
        $problemDeviceNames = [System.Collections.ArrayList]::new()
        try {
            $devices = @(Get-CimInstance Win32_PnPEntity -ErrorAction Stop)
            $deviceCount = ($devices | Measure-Object).Count
            foreach ($dev in $devices) {
                if ($null -ne $dev.HardwareID) { foreach ($hwid in @($dev.HardwareID)) { if ($hwid) { [void]$ids.Add($hwid) } } }
                if ($null -ne $dev.CompatibleID) { foreach ($cid in @($dev.CompatibleID)) { if ($cid) { [void]$ids.Add($cid) } } }
                if ($null -ne $dev.ConfigManagerErrorCode -and $dev.ConfigManagerErrorCode -ne 0) {
                    $devName = $dev.Name; if (-not $devName) { $devName = $dev.DeviceID }
                    [void]$problemDeviceNames.Add("$devName (error $($dev.ConfigManagerErrorCode))")
                }
            }
        }
        catch { Write-Status WARN "WMI scan failed: $($_.Exception.Message)" }
        if ($ids.Count -eq 0) {
            try {
                $pnpDevs = @(Get-PnpDevice -ErrorAction SilentlyContinue)
                $deviceCount = ($pnpDevs | Measure-Object).Count
                foreach ($dev in $pnpDevs) {
                    try {
                        $hwProp = $dev | Get-PnpDeviceProperty -KeyName 'DEVPKEY_Device_HardwareIds' -ErrorAction SilentlyContinue
                        if ($null -ne $hwProp -and $null -ne $hwProp.Data) { foreach ($id in @($hwProp.Data)) { if ($id) { [void]$ids.Add($id) } } }
                    } catch { }
                    try {
                        $cProp = $dev | Get-PnpDeviceProperty -KeyName 'DEVPKEY_Device_CompatibleIds' -ErrorAction SilentlyContinue
                        if ($null -ne $cProp -and $null -ne $cProp.Data) { foreach ($id in @($cProp.Data)) { if ($id) { [void]$ids.Add($id) } } }
                    } catch { }
                }
            } catch { }
        }
        Write-Status OK "Scanned $deviceCount device(s), $($ids.Count) unique ID(s)."
        $probCount = ($problemDeviceNames | Measure-Object).Count
        if ($probCount -gt 0) {
            Write-Status WARN "$probCount device(s) with problems:"
            foreach ($pd in $problemDeviceNames) { Write-Status INFO "  - $pd" }
        }
        return $ids
    }
    
    function Get-InfHardwareIds {
        param([Parameter(Mandatory)][string]$InfPath)
        $hardwareIds = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        try { $lines = @(Get-Content $InfPath -ErrorAction Stop) } catch { return @($hardwareIds) }
        $lineCount = ($lines | Measure-Object).Count
        if ($lineCount -eq 0) { return @($hardwareIds) }
        $modelSections = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        $inManufacturer = $false
        foreach ($line in $lines) {
            $trimmed = $line.Trim()
            if ($trimmed.StartsWith(';')) { continue }
            if ($trimmed -match '^\[Manufacturer\]\s*$') { $inManufacturer = $true; continue }
            if ($inManufacturer) {
                if ($trimmed -match '^\[') { $inManufacturer = $false; continue }
                if (-not $trimmed) { continue }
                if ($trimmed -match '=') {
                    $rightSide = ($trimmed -split '=', 2)[1]
                    if ($null -eq $rightSide) { continue }
                    $parts = $rightSide -split ','
                    $baseName = $parts[0].Trim()
                    if (-not $baseName) { continue }
                    [void]$modelSections.Add($baseName)
                    $partCount = ($parts | Measure-Object).Count
                    for ($j = 1; $j -lt $partCount; $j++) {
                        $decoration = $parts[$j].Trim()
                        if ($decoration) { [void]$modelSections.Add("$baseName.$decoration") }
                    }
                }
            }
        }
        foreach ($section in @($modelSections)) {
            $sectionHeader = "[$section]"
            $inSection = $false
            foreach ($line in $lines) {
                $trimmed = $line.Trim()
                if ($trimmed.StartsWith(';')) { continue }
                if ($trimmed -ieq $sectionHeader) { $inSection = $true; continue }
                if ($inSection) {
                    if ($trimmed -match '^\[') { $inSection = $false; continue }
                    if (-not $trimmed) { continue }
                    if ($trimmed -match '=') {
                        $rightSide = ($trimmed -split '=', 2)[1]
                        if ($null -eq $rightSide) { continue }
                        $idParts = $rightSide -split ','
                        $idPartCount = ($idParts | Measure-Object).Count
                        for ($j = 1; $j -lt $idPartCount; $j++) {
                            $hwid = $idParts[$j].Trim().Trim('"').Trim()
                            if ($hwid -and $hwid -match '\\') { [void]$hardwareIds.Add($hwid) }
                        }
                    }
                }
            }
        }
        return @($hardwareIds)
    }
    
    function Test-InfMatchesSystem {
        param([Parameter(Mandatory)][string]$InfPath, [Parameter(Mandatory)][System.Collections.Generic.HashSet[string]]$SystemIds)
        $infIds = Get-InfHardwareIds -InfPath $InfPath
        $infIdCount = ($infIds | Measure-Object).Count
        if ($infIdCount -eq 0) { return $true }
        foreach ($infId in $infIds) { if ($SystemIds.Contains($infId)) { return $true } }
        return $false
    }
    
    #===================================================================
    #  STATE-MANAGEMENT
    #===================================================================
    function Get-ScriptState {
        if (Test-Path $Script:StateFile) {
            try {
                $json = Get-Content $Script:StateFile -Raw -ErrorAction Stop | ConvertFrom-Json
                $defaults = @{
                    Phase = 0; USBCompleted = $false; USBRebootDone = $false
                    NetworkShareCompleted = $false; NetworkShareRebootDone = $false
                    NetworkSharePath = ""; NetworkShareProcessedDrivers = @()
                    USBProcessedDrivers = @(); USBDriverRoot = ""
                    ManufacturerPhase = 0; DellADRFailed = $false; LastExitCode = 0
                    RebootCount = 0; TotalDriversAdded = 0; TotalDriversInstalled = 0
                    TotalDriversFailed = 0; TotalDriversSkipped = 0
                    TotalDriversTimedOut = 0; TotalTimeSpent = 0; TotalProcessesKilled = 0
                    SystemManufacturer = ""; SystemModel = ""; CleanModel = ""
                    GuestAuthConfigured = $false
                }
                foreach ($k in $defaults.Keys) {
                    if (-not $json.PSObject.Properties[$k]) {
                        $json | Add-Member -NotePropertyName $k -NotePropertyValue $defaults[$k] -Force
                    }
                }
                return $json
            }
            catch { Write-Status WARN "State file error - fresh start: $($_.Exception.Message)" }
        }
        return [PSCustomObject]@{
            Phase = 0; USBCompleted = $false; USBRebootDone = $false
            NetworkShareCompleted = $false; NetworkShareRebootDone = $false
            NetworkSharePath = ""; NetworkShareProcessedDrivers = @()
            USBProcessedDrivers = @(); USBDriverRoot = ""
            ManufacturerPhase = 0; DellADRFailed = $false; LastExitCode = 0
            RebootCount = 0; TotalDriversAdded = 0; TotalDriversInstalled = 0
            TotalDriversFailed = 0; TotalDriversSkipped = 0
            TotalDriversTimedOut = 0; TotalTimeSpent = 0; TotalProcessesKilled = 0
            SystemManufacturer = ""; SystemModel = ""; CleanModel = ""
            GuestAuthConfigured = $false
        }
    }
    
    function Set-ScriptState {
        param([pscustomobject]$State)
        $State | ConvertTo-Json -Depth 10 | Out-File $Script:StateFile -Encoding UTF8 -Force
    }
    
    function Remove-ScriptState {
        Write-Status INFO "Removing state and RunOnce..."
        if (Test-Path $Script:StateFile) { Remove-Item $Script:StateFile -Force -ErrorAction SilentlyContinue }
        try {
            $rk64 = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64).OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', $true)
            if ($null -ne $rk64) { $rk64.DeleteValue($Script:RunOnceName, $false); $rk64.Close() }
        } catch { }
        $fallback = "$($Script:RunOnceName)_Task"
        try {
            $t = Get-ScheduledTask -TaskName $fallback -ErrorAction SilentlyContinue
            if ($null -ne $t) { Unregister-ScheduledTask -TaskName $fallback -Confirm:$false -ErrorAction SilentlyContinue }
        } catch { }
        Write-Status OK "Cleanup finished."
    }
    
    #===================================================================
    #  RUNONCE & REBOOT
    #===================================================================
    function Set-RunOnceEntry {
        param([Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$Command)
        try {
            $rk64 = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64).OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', $true)
            $rk64.SetValue($Name, $Command, [Microsoft.Win32.RegistryValueKind]::String); $rk64.Close()
            Write-Status OK "RunOnce: $Name"; return $true
        }
        catch { Write-Status WARN "RunOnce failed: $($_.Exception.Message)"; return $false }
    }
    
    function Set-RunOnceTask {
        param([Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$ScriptPath)
        try {
            $a = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoLogo -NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
            $tr = New-ScheduledTaskTrigger -AtStartup
            $p = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest -LogonType ServiceAccount
            Register-ScheduledTask -TaskName $Name -Action $a -Trigger $tr -Principal $p -Force -ErrorAction Stop
            Write-Status OK "Fallback task: $Name"; return $true
        }
        catch { Write-Status ERROR "Task failed: $($_.Exception.Message)"; return $false }
    }
    
    function Schedule-RebootAndContinue {
        param([pscustomobject]$State, [string]$Reason = "Reboot required")
        Write-LoggedHost
        Write-LoggedHost "======================================================================" -ForegroundColor Yellow
        Write-LoggedHost "  REBOOT REQUIRED: $Reason" -ForegroundColor Yellow
        Write-LoggedHost "======================================================================" -ForegroundColor Yellow
        Write-LoggedHost
        Stop-DriverInstallProcesses -Silent | Out-Null
        $State.RebootCount++; Set-ScriptState -State $State
        $cmd = "powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$Script:PersistentScript`""
        $ok = Set-RunOnceEntry -Name $Script:RunOnceName -Command $cmd
        if (-not $ok) { Set-RunOnceTask -Name "$($Script:RunOnceName)_Task" -ScriptPath $Script:PersistentScript | Out-Null }
        for ($i = 15; $i -ge 1; $i--) {
            Write-LoggedHost "  Rebooting in $i seconds..." -ForegroundColor Yellow
            Start-Sleep -Seconds 1
        }
        Write-LoggedHost "  Rebooting NOW!" -ForegroundColor Red
        try { Stop-Transcript } catch { }
        Restart-Computer -Force; exit 0
    }
    
    #===================================================================
    #  SINGLE DRIVER INSTALL
    #===================================================================
    function Install-SingleDriver {
        param(
            [Parameter(Mandatory)][string]$InfPath,
            [Parameter(Mandatory)][string]$DriverName,
            [Parameter(Mandatory)][int]$CurrentNumber,
            [Parameter(Mandatory)][int]$TotalCount,
            [int]$TimeoutSeconds = $Script:DriverTimeout
        )
        $result = [PSCustomObject]@{
            InfPath = $InfPath; DriverName = $DriverName; Success = $false
            ExitCode = -1; Added = $false; Installed = $false; AlreadyExists = $false
            NeedsReboot = $false; TimedOut = $false; ProcessesKilled = 0
            ErrorMessage = ""; Output = ""; Duration = 0
        }
        Write-DriverHeader -DriverName $DriverName -Current $CurrentNumber -Total $TotalCount
        Write-Status DRIVER "Path: $InfPath"
        Clear-DriverInstallEnvironment
        Write-Status INFO "Starting pnputil installation..."
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        $consoleWidth = Get-ConsoleWidth
        $tempScript = [IO.Path]::GetTempFileName() + ".ps1"
        $outFile = [IO.Path]::GetTempFileName()
        $exitFile = [IO.Path]::GetTempFileName()
        $wrapperContent = @"
    try {
        `$out = & pnputil.exe /add-driver "$InfPath" /install 2>&1 | Out-String
        `$out | Out-File -FilePath "$outFile" -Encoding UTF8
        `$LASTEXITCODE | Out-File -FilePath "$exitFile" -Encoding UTF8
    } catch {
        `$_.Exception.Message | Out-File -FilePath "$outFile" -Encoding UTF8
        "999" | Out-File -FilePath "$exitFile" -Encoding UTF8
    }
    "@
        $wrapperContent | Out-File -FilePath $tempScript -Encoding UTF8
        $proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$tempScript`"" -PassThru -WindowStyle Hidden
        $frameIdx = 0; $frameCount = $Script:SpinnerFrames.Length
        $animLine = 0; try { $animLine = [Console]::CursorTop } catch { }
        Write-Host "[PNPUTIL] Installing: $DriverName ($CurrentNumber/$TotalCount)" -ForegroundColor Cyan
        while ((-not $proc.HasExited) -and ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds)) {
            $elapsed = $stopwatch.Elapsed.TotalSeconds
            $remaining = [math]::Max(0, $TimeoutSeconds - $elapsed)
            $pct = [math]::Round(($elapsed / $TimeoutSeconds) * 100)
            $spinner = $Script:SpinnerFrames[$frameIdx % $frameCount]; $frameIdx++
            $line = "  $spinner $pct% | Elapsed: $([math]::Round($elapsed))s | Remaining: $([math]::Round($remaining))s"
            $padded = $line.PadRight($consoleWidth)
            try {
                [Console]::SetCursorPosition(0, $animLine)
                [Console]::ForegroundColor = [ConsoleColor]::Cyan
                [Console]::Write($padded); [Console]::ResetColor()
            }
            catch { Write-Host "`r$padded" -NoNewline -ForegroundColor Cyan }
            Start-Sleep -Milliseconds 150
        }
        try { [Console]::SetCursorPosition(0, $animLine); [Console]::Write(' ' * $consoleWidth); [Console]::SetCursorPosition(0, $animLine) }
        catch { Write-Host "`r$(' ' * $consoleWidth)`r" -NoNewline }
        $stopwatch.Stop()
        $result.Duration = $stopwatch.Elapsed.TotalSeconds
        if (-not $proc.HasExited) {
            Write-Status SKIP "TIMEOUT after $(Format-Duration $TimeoutSeconds)!"
            try { $proc.Kill() } catch { }
            $result.TimedOut = $true; $result.ErrorMessage = "Timeout"
            $killed = Stop-DriverInstallProcesses; $result.ProcessesKilled = $killed
            $null = Wait-ForDriverProcesses -TimeoutSeconds 10
            Remove-Item $tempScript, $outFile, $exitFile -Force -ErrorAction SilentlyContinue
            Write-Host "[PNPUTIL] TIMEOUT for $DriverName after $TimeoutSeconds seconds" -ForegroundColor Yellow
            if ($Script:CooldownSeconds -gt 0) { Start-Sleep -Seconds $Script:CooldownSeconds }
            return $result
        }
        $out = ""; $exit = -1
        if (Test-Path $outFile) { $out = Get-Content $outFile -Raw -ErrorAction SilentlyContinue }
        if (Test-Path $exitFile) {
            $raw = Get-Content $exitFile -Raw -ErrorAction SilentlyContinue
            if ($null -ne $raw) { [int]::TryParse($raw.Trim(), [ref]$exit) | Out-Null }
        }
        Remove-Item $tempScript, $outFile, $exitFile -Force -ErrorAction SilentlyContinue
        $result.ExitCode = $exit; $result.Output = $out
        if ($null -ne $out -and $out.Trim()) {
            Write-Host "[PNPUTIL] --- Output for $DriverName ---" -ForegroundColor DarkGray
            foreach ($outLine in ($out -split "`n")) {
                $trimLine = $outLine.Trim()
                if ($trimLine) { Write-Host "[PNPUTIL] $trimLine" -ForegroundColor DarkGray }
            }
            Write-Host "[PNPUTIL] --- End output ---" -ForegroundColor DarkGray
        }
        if ($null -ne $out) {
            if ($out -match 'added|successfully added|driver package added') { $result.Added = $true; Write-Status OK "Driver added to store" }
            if ($out -match 'install.*device|installed to device') { $result.Installed = $true; Write-Status OK "Driver installed on device" }
            if ($out -match 'already exists|up to date') { $result.AlreadyExists = $true; Write-Status INFO "Already up-to-date" }
            if ($out -match 'reboot|restart') { $result.NeedsReboot = $true; Write-Status WARN "Reboot required" }
        }
        switch ($exit) {
            0    { $result.Success = $true; Write-Status OK "Exit 0 - Success" }
            1    { $result.Success = $true; Write-Status INFO "Exit 1 - Warnings" }
            259  { $result.Success = $true; Write-Status INFO "Exit 259 - Staged" }
            3010 { $result.Success = $true; $result.NeedsReboot = $true; Write-Status WARN "Exit 3010 - Reboot" }
            482  { $result.Success = $true; $result.NeedsReboot = $true; Write-Status WARN "Exit 482 - Partial" }
            default {
                if ($exit -ge 0) {
                    if ($result.Added -or $result.Installed -or $result.AlreadyExists) { $result.Success = $true }
                    else { $result.Success = $false; $result.ErrorMessage = "Exit $exit"; Write-Status ERROR "Exit $exit" }
                } else { $result.Success = $true }
            }
        }
        Write-Status TIME "Time: $(Format-Duration $result.Duration)"
        if ($Script:CooldownSeconds -gt 0) { Start-Sleep -Seconds $Script:CooldownSeconds }
        return $result
    }
    
    #===================================================================
    #  GENERIC FOLDER INSTALLER
    #===================================================================
    function Install-DriversFromFolder {
        param(
            [Parameter(Mandatory)][string]$DriverRoot,
            [Parameter(Mandatory)][string]$Label,
            [string[]]$ProcessedList = @(),
            [switch]$SkipDeviceMatching,
            [System.Collections.Generic.HashSet[string]]$SystemIds = $null
        )
        $result = [PSCustomObject]@{
            Success = $false; NeedsReboot = $false; NotFound = $false
            TotalFound = 0; TotalMatched = 0; TotalSkippedNoMatch = 0
            TotalAdded = 0; TotalInstalled = 0; TotalFailed = 0
            TotalAlreadyExist = 0; TotalTimedOut = 0; TotalTime = 0; TotalKilled = 0
            ProcessedDrivers = @($ProcessedList)
        }
        $allInfFiles = @(Get-ChildItem -Path $DriverRoot -Filter "*.inf" -Recurse -File -ErrorAction SilentlyContinue | Sort-Object FullName)
        $totalFound = ($allInfFiles | Measure-Object).Count
        if ($totalFound -eq 0) {
            Write-Status WARN "No .inf files in: $DriverRoot"
            $result.NotFound = $true; $result.Success = $true; return $result
        }
        $result.TotalFound = $totalFound
        Write-Status OK "Found $totalFound INF(s) in: $DriverRoot"
        $processed = @($ProcessedList)
        $processedCount = ($processed | Measure-Object).Count
        if ($processedCount -gt 0) { Write-Status INFO "$processedCount already processed." }
        $matchedInfs = [System.Collections.ArrayList]::new()
        $skippedNoMatch = [System.Collections.ArrayList]::new()
        if ($SkipDeviceMatching) {
            Write-Status INFO "Device matching DISABLED - installing ALL."
            foreach ($inf in $allInfFiles) { [void]$matchedInfs.Add($inf) }
        }
        else {
            if ($null -eq $SystemIds -or $SystemIds.Count -eq 0) {
                Write-SubBanner "Scanning System Devices"
                $SystemIds = Get-SystemDeviceIds
            }
            if ($SystemIds.Count -eq 0) {
                Write-Status WARN "No device IDs - installing ALL as fallback."
                foreach ($inf in $allInfFiles) { [void]$matchedInfs.Add($inf) }
            }
            else {
                Write-SubBanner "Matching $Label Drivers to Devices"
                foreach ($inf in $allInfFiles) {
                    $rel = $inf.FullName.Substring($DriverRoot.Length + 1)
                    if ($rel -in $processed) { continue }
                    if (Test-InfMatchesSystem -InfPath $inf.FullName -SystemIds $SystemIds) { [void]$matchedInfs.Add($inf) }
                    else { [void]$skippedNoMatch.Add($inf); Write-Status SKIP "No match: $rel" }
                }
            }
        }
        $matchedCount = ($matchedInfs | Measure-Object).Count
        $skippedNoMatchCount = ($skippedNoMatch | Measure-Object).Count
        $result.TotalMatched = $matchedCount; $result.TotalSkippedNoMatch = $skippedNoMatchCount
        $modeLabel = if ($SkipDeviceMatching) { "ALL (no filtering)" } else { "TARGETED" }
        $modeColor = if ($SkipDeviceMatching) { 'Yellow' } else { 'Green' }
        Write-LoggedHost
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost "              $Label - MATCHING RESULTS" -ForegroundColor Cyan
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost "  Total INFs:       $totalFound" -ForegroundColor White
        Write-LoggedHost "  Processed:        $processedCount" -ForegroundColor DarkGray
        Write-LoggedHost "  Matched:          $matchedCount" -ForegroundColor Green
        Write-LoggedHost "  Skipped:          $skippedNoMatchCount" -ForegroundColor Yellow
        Write-LoggedHost "  Mode:             $modeLabel" -ForegroundColor $modeColor
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost
        if ($matchedCount -eq 0) {
            Write-Status OK "No new drivers to install."; $result.Success = $true; return $result
        }
        $overallSw = [System.Diagnostics.Stopwatch]::StartNew()
        $globalReboot = $false; $i = 0
        Write-SubBanner "Installing $matchedCount $Label Driver(s)"
        foreach ($inf in $matchedInfs) {
            $i++
            $rel = $inf.FullName.Substring($DriverRoot.Length + 1)
            if ($rel -in $processed) { continue }
            $drvResult = Install-SingleDriver -InfPath $inf.FullName -DriverName $rel -CurrentNumber $i -TotalCount $matchedCount
            $result.TotalTime += $drvResult.Duration; $result.TotalKilled += $drvResult.ProcessesKilled
            if ($drvResult.TimedOut) { $result.TotalTimedOut++ } elseif ($drvResult.Added) { $result.TotalAdded++ }
            if ($drvResult.Installed) { $result.TotalInstalled++ }
            if ($drvResult.AlreadyExists) { $result.TotalAlreadyExist++ }
            if ((-not $drvResult.Success) -and (-not $drvResult.TimedOut)) { $result.TotalFailed++ }
            if ($drvResult.NeedsReboot) { $globalReboot = $true }
            $processed += $rel; $result.ProcessedDrivers = @($processed)
        }
        $overallSw.Stop()
        $failColor = if ($result.TotalFailed -gt 0) { 'Red' } else { 'Green' }
        $timeoutColor = if ($result.TotalTimedOut -gt 0) { 'Yellow' } else { 'Green' }
        $killColor = if ($result.TotalKilled -gt 0) { 'Yellow' } else { 'Green' }
        $rebootColor = if ($globalReboot) { 'Yellow' } else { 'Green' }
        Write-LoggedHost
        Write-LoggedHost "======================================================================" -ForegroundColor Green
        Write-LoggedHost "              $Label DRIVER SUMMARY" -ForegroundColor Green
        Write-LoggedHost "======================================================================" -ForegroundColor Green
        Write-LoggedHost "  Found:      $($result.TotalFound)" -ForegroundColor White
        Write-LoggedHost "  Matched:    $($result.TotalMatched)" -ForegroundColor Green
        Write-LoggedHost "  Skipped:    $($result.TotalSkippedNoMatch)" -ForegroundColor Yellow
        Write-LoggedHost "  Added:      $($result.TotalAdded)" -ForegroundColor Green
        Write-LoggedHost "  Installed:  $($result.TotalInstalled)" -ForegroundColor Green
        Write-LoggedHost "  Up-to-date: $($result.TotalAlreadyExist)" -ForegroundColor Cyan
        Write-LoggedHost "  Failed:     $($result.TotalFailed)" -ForegroundColor $failColor
        Write-LoggedHost "  Timed out:  $($result.TotalTimedOut)" -ForegroundColor $timeoutColor
        Write-LoggedHost "  Killed:     $($result.TotalKilled)" -ForegroundColor $killColor
        Write-LoggedHost "  Time:       $(Format-Duration $overallSw.Elapsed.TotalSeconds)" -ForegroundColor Magenta
        Write-LoggedHost "  Reboot:     $globalReboot" -ForegroundColor $rebootColor
        Write-LoggedHost "======================================================================" -ForegroundColor Green
        Write-LoggedHost
        $result.Success = $true; $result.NeedsReboot = $globalReboot
        return $result
    }
    
    #===================================================================
    #  PHASE-0A: USB DRIVERS (TARGETED)
    #===================================================================
    function Install-USBNetworkDrivers {
        param([pscustomobject]$State)
        Write-Banner "PHASE-0A: USB DRIVER INSTALLATION (TARGETED)"
        Write-Status STEP "Installing ONLY drivers matching this PC's devices from USB."
        Clear-DriverInstallEnvironment
        $root = $null
        if ($State.USBDriverRoot -and (Test-Path $State.USBDriverRoot)) {
            $root = $State.USBDriverRoot
        }
        else {
            foreach ($drive in @('D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z')) {
                foreach ($folder in $Script:USBDriverFolders) {
                    $candidate = "${drive}:\$folder"
                    if (Test-Path $candidate) { $root = $candidate; Write-Status OK "Found USB driver folder: $root"; break }
                }
                if ($null -ne $root) { break }
            }
        }
        if ($null -eq $root) {
            Write-Status INFO "No USB driver folder found - skipping USB phase."
            return [PSCustomObject]@{ Success = $true; NeedsReboot = $false; NotFound = $true }
        }
        $State.USBDriverRoot = $root; Set-ScriptState -State $State
        Write-SubBanner "Scanning System Devices for USB Driver Matching"
        $systemIds = Get-SystemDeviceIds
        Write-Status INFO "Will match USB INF files against $($systemIds.Count) hardware ID(s)."
        $processed = @(); if ($State.USBProcessedDrivers) { $processed = @($State.USBProcessedDrivers) }
        $usbResult = Install-DriversFromFolder -DriverRoot $root -Label "USB" -ProcessedList $processed -SystemIds $systemIds
        $State.USBProcessedDrivers     = $usbResult.ProcessedDrivers
        $State.TotalDriversAdded      += $usbResult.TotalAdded
        $State.TotalDriversInstalled  += $usbResult.TotalInstalled
        $State.TotalDriversFailed     += $usbResult.TotalFailed
        $State.TotalDriversSkipped    += $usbResult.TotalSkippedNoMatch
        $State.TotalDriversTimedOut   += $usbResult.TotalTimedOut
        $State.TotalTimeSpent         += $usbResult.TotalTime
        $State.TotalProcessesKilled   += $usbResult.TotalKilled
        Set-ScriptState -State $State
        return $usbResult
    }
    
    #===================================================================
    #  PHASE-0B: NETWORK SHARE DRIVERS
    #===================================================================
    function Install-NetworkShareDrivers {
        param([pscustomobject]$State)
        Write-Banner "PHASE-0B: NETWORK SHARE DRIVER INSTALLATION"
        if (-not $State.GuestAuthConfigured) {
            Enable-GuestNetworkAccess | Out-Null
            $State.GuestAuthConfigured = $true; Set-ScriptState -State $State
        }
        else { Write-Status OK "SMB guest access already configured." }
        Write-SubBanner "Identifying System Model"
        $sysInfo = Get-SystemInfo
        $State.SystemManufacturer = $sysInfo.RawManufacturer
        $State.SystemModel = $sysInfo.RawModel
        $State.CleanModel = $sysInfo.CleanModel
        Set-ScriptState -State $State
        Write-Status INFO "Manufacturer: $($sysInfo.RawManufacturer)"
        Write-Status INFO "Model:        $($sysInfo.RawModel)"
        Write-Status INFO "Clean model:  $($sysInfo.CleanModel)"
        $mfgFolder = Get-ManufacturerShareFolder -Manufacturer $sysInfo.RawManufacturer
        $sharePath = ""
        if ($State.NetworkSharePath -and (Test-Path $State.NetworkSharePath -ErrorAction SilentlyContinue)) {
            $sharePath = $State.NetworkSharePath
        }
        else { $sharePath = Build-NetworkDriverPath -ManufacturerFolder $mfgFolder -CleanModel $sysInfo.CleanModel }
        Write-LoggedHost
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost "              NETWORK SHARE PATH" -ForegroundColor Cyan
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost "  Base:    $($Script:NetworkShareBase)" -ForegroundColor White
        Write-LoggedHost "  Mfg:     $mfgFolder" -ForegroundColor White
        Write-LoggedHost "  Model:   $($sysInfo.CleanModel)" -ForegroundColor White
        Write-LoggedHost "  Path:    $sharePath" -ForegroundColor Green
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost
        Write-SubBanner "Testing Network Share Access"
        $shareOk = Test-NetworkShareAccess -SharePath $sharePath -MaxRetries 10 -RetryDelaySec 10
        if (-not $shareOk) {
            $alternatives = @(($sysInfo.RawModel -replace '\s', '-').ToLower(), ($sysInfo.RawModel -replace '\s', '_').ToLower(), $sysInfo.RawModel.ToLower())
            $altFound = $false
            foreach ($alt in $alternatives) {
                $altPath = Build-NetworkDriverPath -ManufacturerFolder $mfgFolder -CleanModel $alt
                Write-Status INFO "Trying: $altPath"
                if (Test-Path $altPath -ErrorAction SilentlyContinue) {
                    $sharePath = $altPath; Write-Status OK "Found: $sharePath"; $altFound = $true; break
                }
            }
            if (-not $altFound) {
                Write-Status ERROR "No share folder found. Expected: $sharePath"
                return [PSCustomObject]@{ Success = $true; NeedsReboot = $false; NotFound = $true }
            }
        }
        $State.NetworkSharePath = $sharePath; Set-ScriptState -State $State
        $processed = @(); if ($State.NetworkShareProcessedDrivers) { $processed = @($State.NetworkShareProcessedDrivers) }
        Write-SubBanner "Scanning Devices"
        $systemIds = Get-SystemDeviceIds
        $netResult = Install-DriversFromFolder -DriverRoot $sharePath -Label "NETWORK SHARE" -ProcessedList $processed -SystemIds $systemIds
        $State.NetworkShareProcessedDrivers = $netResult.ProcessedDrivers
        $State.TotalDriversAdded      += $netResult.TotalAdded
        $State.TotalDriversInstalled  += $netResult.TotalInstalled
        $State.TotalDriversFailed     += $netResult.TotalFailed
        $State.TotalDriversSkipped    += $netResult.TotalSkippedNoMatch
        $State.TotalDriversTimedOut   += $netResult.TotalTimedOut
        $State.TotalTimeSpent         += $netResult.TotalTime
        $State.TotalProcessesKilled   += $netResult.TotalKilled
        Set-ScriptState -State $State
        return $netResult
    }
    
    #===================================================================
    #  INTERNET & MANUFACTURER
    #===================================================================
    function Test-InternetConnectivity {
        param([int]$MaxRetries = 15, [int]$RetryDelay = 10)
        Write-Banner "CHECKING INTERNET CONNECTIVITY"
        $w = Get-ConsoleWidth; $totalFrames = $Script:SpinnerFrames.Length
        for ($i = 1; $i -le $MaxRetries; $i++) {
            Write-Status PROGRESS "Attempt $i of $MaxRetries..."
            try {
                $r = Invoke-WebRequest -Uri "https://www.google.com" -Method Head -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
                if ($r.StatusCode -eq 200) { Write-Status OK "Internet reachable."; return $true }
            }
            catch {
                Write-Status WARN "Attempt $i failed: $($_.Exception.Message)"
                if ($i -lt $MaxRetries) {
                    for ($s = $RetryDelay; $s -ge 1; $s--) {
                        $spinner = $Script:SpinnerFrames[($RetryDelay - $s) % $totalFrames]
                        $pad = "  $spinner Waiting $s sec...".PadRight($w)
                        try { [Console]::SetCursorPosition(0, [Console]::CursorTop); [Console]::ForegroundColor = [ConsoleColor]::DarkGray; [Console]::Write($pad); [Console]::ResetColor() }
                        catch { Write-Host "`r$pad" -NoNewline -ForegroundColor DarkGray }
                        Start-Sleep -Seconds 1
                    }
                }
            }
        }
        Write-Status ERROR "No internet after $MaxRetries attempts."
        return $false
    }
    
    function Get-SystemManufacturer {
        Write-SubBanner "Detecting System Manufacturer"
        $cs = Get-CimInstance Win32_ComputerSystem
        Write-Status INFO "Manufacturer: $($cs.Manufacturer.Trim())"
        Write-Status INFO "Model:        $($cs.Model.Trim())"
        return $cs.Manufacturer.Trim()
    }
    
    #===================================================================
    #  CHOCOLATEY
    #===================================================================
    function Set-ChocolateySource {
        Write-SubBanner "Configuring Chocolatey Sources"
        $chocoExe = Get-ChocoExePath; if (-not $chocoExe) { return }
        $null = Invoke-CommandVerbose -FilePath $chocoExe -Arguments "source add -n `"$($Script:ChocoSourceName)`" -s `"$($Script:ChocoSourceUrl)`" --priority=1 --allow-self-service" -Label "CHOCO"
        $null = Invoke-CommandVerbose -FilePath $chocoExe -Arguments "source disable -n `"chocolatey`"" -Label "CHOCO"
        $null = Invoke-CommandVerbose -FilePath $chocoExe -Arguments "source list" -Label "CHOCO"
    }
    
    function Install-ChocolateyIfNeeded {
        Write-SubBanner "Ensuring Chocolatey"
        if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
            Write-Status INFO "Installing Chocolatey..."
            Write-Host "[CHOCO] Installing Chocolatey from community.chocolatey.org..." -ForegroundColor Cyan
            Set-ExecutionPolicy Bypass -Scope Process -Force
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072
            Invoke-Expression (Invoke-RestMethod https://community.chocolatey.org/install.ps1)
            $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
            Write-Status OK "Chocolatey installed."
        }
        else { Write-Status OK "Chocolatey present." }
        Set-ChocolateySource
    }
    
    function Install-ChocoPackage {
        param([Parameter(Mandatory)][string]$PackageName, [string]$DisplayName)
        if (-not $DisplayName) { $DisplayName = $PackageName }
        $chocoExe = Get-ChocoExePath; if (-not $chocoExe) { return $false }
        Write-SubBanner "Installing $DisplayName via Chocolatey"
        $exitCode = Invoke-CommandVerbose -FilePath $chocoExe -Arguments "install $PackageName -y --source=`"$($Script:ChocoSourceName)`"" -Label "CHOCO"
        if ($exitCode -ne 0) {
            Write-Status WARN "Custom source failed - trying default..."
            $exitCode = Invoke-CommandVerbose -FilePath $chocoExe -Arguments "install $PackageName -y" -Label "CHOCO"
        }
        if ($exitCode -eq 0) { Write-Status OK "$DisplayName installed."; return $true }
        else { Write-Status ERROR "$DisplayName failed."; return $false }
    }
    
    #===================================================================
    #  DELL UPDATES
    #===================================================================
    function Find-DellCLI {
        $knownPaths = @(
            'C:\Program Files\Dell\CommandUpdate\dcu-cli.exe'
            'C:\Program Files (x86)\Dell\CommandUpdate\dcu-cli.exe'
            'C:\Program Files\Dell\Dell Command Update\dcu-cli.exe'
            'C:\Program Files (x86)\Dell\Dell Command Update\dcu-cli.exe'
        )
        return Find-Executable -FileName 'dcu-cli.exe' -KnownPaths $knownPaths -SearchRoots @('C:\Dell') -Label "Dell Command Update CLI"
    }
    
    function Invoke-DellUpdates {
        param([pscustomobject]$State)
        Write-Banner "DELL SYSTEM UPDATE"
    
        $cli = Find-DellCLI
        if (-not $cli) {
            Install-ChocoPackage -PackageName "dellcommandupdate" -DisplayName "Dell Command Update" | Out-Null
            Start-Sleep -Seconds 5
            $cli = Find-DellCLI
        }
        if (-not $cli) { Write-Status ERROR "Dell CLI not found anywhere."; return $false }
        Write-Status OK "Dell CLI: $cli"
    
        switch ($State.ManufacturerPhase) {
            0 {
                if ($State.DellADRFailed) { $State.ManufacturerPhase = 2; Set-ScriptState -State $State; return (Invoke-DellUpdates -State $State) }
                Write-SubBanner "Dell Phase-0: Configure ADR"
                $null = Invoke-CommandVerbose -FilePath $cli -Arguments "/configure -advancedDriverRestore=enable" -Label "DCU-CFG"
                Write-SubBanner "Dell Phase-0: ADR Driver Install"
                Write-Status STEP "All driver names and progress will appear below."
                $dellExit = Invoke-CommandVerbose -FilePath $cli -Arguments "/driverinstall" -Label "DCU"
                switch ($dellExit) {
                    0 { $State.ManufacturerPhase = 2; Set-ScriptState -State $State; return (Invoke-DellUpdates -State $State) }
                    1 { $State.ManufacturerPhase = 1; Schedule-RebootAndContinue -State $State -Reason "Dell ADR (exit 1)" }
                    5 { $State.ManufacturerPhase = 1; Schedule-RebootAndContinue -State $State -Reason "Dell ADR (exit 5)" }
                    2 { $State.DellADRFailed = $true; $State.ManufacturerPhase = 2; Set-ScriptState -State $State; return (Invoke-DellUpdates -State $State) }
                    3 { $State.ManufacturerPhase = 2; Set-ScriptState -State $State; return (Invoke-DellUpdates -State $State) }
                    default { $State.DellADRFailed = $true; $State.ManufacturerPhase = 2; Set-ScriptState -State $State; return (Invoke-DellUpdates -State $State) }
                }
            }
            1 {
                Write-SubBanner "Dell Phase-1: Post-Reboot Scan"
                $null = Invoke-CommandVerbose -FilePath $cli -Arguments "/scan" -Label "DCU"
                $State.ManufacturerPhase = 2; Set-ScriptState -State $State; return (Invoke-DellUpdates -State $State)
            }
            2 {
                Write-SubBanner "Dell Phase-2: Apply All Updates"
                Write-Status STEP "Each update name, download, and install status shown below."
                $dellExit = Invoke-CommandVerbose -FilePath $cli -Arguments "/applyupdates -forceUpdate=enable -autoSuspendBitLocker=enable" -Label "DCU"
                switch ($dellExit) {
                    0 { Write-Status OK "All updates applied."; $State.ManufacturerPhase = 3; Set-ScriptState -State $State; return $true }
                    1 { $State.ManufacturerPhase = 3; Schedule-RebootAndContinue -State $State -Reason "Dell updates (exit 1)" }
                    5 { $State.ManufacturerPhase = 3; Schedule-RebootAndContinue -State $State -Reason "Dell updates (exit 5)" }
                    3 { Write-Status OK "Already up to date."; $State.ManufacturerPhase = 3; Set-ScriptState -State $State; return $true }
                    default { Write-Status WARN "Exit $dellExit - continuing."; $State.ManufacturerPhase = 3; Set-ScriptState -State $State; return $true }
                }
            }
            default { Write-Status OK "Dell updates done."; return $true }
        }
        return $true
    }
    
    #===================================================================
    #  HP UPDATES (FIXED - robust exe discovery)
    #===================================================================
    function Find-HPIA {
        $knownPaths = @(
            # Chocolatey install location (priority - most common after choco install)
            "$env:ProgramData\chocolatey\lib\hpimageassistant\tools\HPImageAssistant.exe"
            # Standard HP install locations
            'C:\HP\HPIA\HPImageAssistant.exe'
            'C:\Program Files\HP\HPIA\HPImageAssistant.exe'
            'C:\Program Files (x86)\HP\HPIA\HPImageAssistant.exe'
            'C:\SWSetup\HPIA\HPImageAssistant.exe'
            'C:\Program Files\HPImageAssistant\HPImageAssistant.exe'
            'C:\Program Files (x86)\HPImageAssistant\HPImageAssistant.exe'
            "$env:ProgramData\HP\HPIA\HPImageAssistant.exe"
            'C:\HP\HPImageAssistant\HPImageAssistant.exe'
        )
        return Find-Executable -FileName 'HPImageAssistant.exe' -KnownPaths $knownPaths -SearchRoots @('C:\HP', 'C:\SWSetup') -Label "HP Image Assistant"
    }
    
    function Invoke-HPUpdates {
        param([pscustomobject]$State)
        Write-Banner "HP SYSTEM UPDATE"
    
        # ── Find HPIA before Chocolatey ──
        Write-SubBanner "Locating HP Image Assistant"
        $hpia = Find-HPIA
    
        # ── Try Chocolatey install if not found ──
        if (-not $hpia) {
            Write-Status WARN "HPIA not found - attempting Chocolatey install..."
    
            # Try multiple known package names
            $chocoPackages = @('hpimageassistant', 'hp-hpia', 'hp-image-assistant')
            $installed = $false
            foreach ($pkg in $chocoPackages) {
                Write-Status INFO "Trying Chocolatey package: $pkg"
                $result = Install-ChocoPackage -PackageName $pkg -DisplayName "HP Image Assistant ($pkg)"
                if ($result) { $installed = $true; break }
            }
    
            if ($installed) {
                Start-Sleep -Seconds 5
                # Refresh PATH
                $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
                $hpia = Find-HPIA
            }
        }
    
        # ── Final validation ──
        if (-not $hpia) {
            Write-Status ERROR "HP Image Assistant (HPImageAssistant.exe) could not be found anywhere."
            Write-Status INFO "Searched: known paths, Chocolatey lib, Program Files, C:\HP, C:\SWSetup"
            Write-Status INFO "Please install HPIA manually and re-run the script."
            return $false
        }
    
        # Verify it is a file, not a directory
        $hpiaItem = Get-Item $hpia -ErrorAction SilentlyContinue
        if ($null -eq $hpiaItem -or $hpiaItem.PSIsContainer) {
            Write-Status ERROR "Path is a directory, not an executable: $hpia"
            return $false
        }
    
        Write-Status OK  "HPIA executable: $hpia"
        Write-Status DEBUG "HPIA size: $([math]::Round($hpiaItem.Length / 1MB, 1)) MB"
        Write-Status DEBUG "HPIA version: $($hpiaItem.VersionInfo.FileVersion)"
    
        # ── Create report folder ──
        $hpiaReport = 'C:\Temp\HPIA'
        if (-not (Test-Path $hpiaReport)) {
            New-Item -Path $hpiaReport -ItemType Directory -Force | Out-Null
        }
    
        # ── Run HPIA ──
        Write-SubBanner "HP Image Assistant: Analyze & Install"
        Write-Status STEP "All update names and progress will appear below."
    
        $hpArgs = "/Operation:Analyze /Action:Install /Selection:All /Silent /Noninteractive /ReportFolder:`"$hpiaReport`""
        $hpExit = Invoke-CommandVerbose -FilePath $hpia -Arguments $hpArgs -Label "HPIA"
    
        # ── Report results ──
        Write-LoggedHost
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost "              HP IMAGE ASSISTANT RESULTS" -ForegroundColor Cyan
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost "  Executable:  $hpia" -ForegroundColor White
        Write-LoggedHost "  Exit code:   $hpExit" -ForegroundColor White
        Write-LoggedHost "  Report:      $hpiaReport" -ForegroundColor White
    
        # Parse report if available
        $jsonReport = Get-ChildItem -Path $hpiaReport -Filter '*.json' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
        if ($jsonReport) {
            Write-LoggedHost "  Report file: $($jsonReport.FullName)" -ForegroundColor Green
            try {
                $reportData = Get-Content $jsonReport.FullName -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json -ErrorAction SilentlyContinue
                if ($null -ne $reportData -and $null -ne $reportData.HPIA -and $null -ne $reportData.HPIA.Recommendations) {
                    $recs = @($reportData.HPIA.Recommendations)
                    $recCount = ($recs | Measure-Object).Count
                    Write-LoggedHost "  Updates:     $recCount recommendation(s)" -ForegroundColor White
                    foreach ($rec in $recs) {
                        $name = if ($rec.Name) { $rec.Name } else { $rec.SoftPaqId }
                        $status = if ($rec.RecommendationValue) { $rec.RecommendationValue } else { 'Unknown' }
                        Write-LoggedHost "    - $name ($status)" -ForegroundColor DarkGray
                    }
                }
            }
            catch { Write-Status DEBUG "Could not parse HPIA report: $($_.Exception.Message)" }
        }
        else {
            Write-LoggedHost "  Report file: (none found)" -ForegroundColor Yellow
        }
        Write-LoggedHost "  ======================================================================" -ForegroundColor DarkCyan
        Write-LoggedHost
    
        switch ($hpExit) {
            0    { Write-Status OK "HP updates completed successfully."; return $true }
            256  { Write-Status WARN "HP: reboot needed for some updates."; return $true }
            3010 { Write-Status WARN "HP: reboot needed (exit 3010)."; return $true }
            1    { Write-Status WARN "HP: completed with warnings."; return $true }
            default { Write-Status WARN "HP exit $hpExit - check report at $hpiaReport"; return $true }
        }
    }
    
    #===================================================================
    #  MAIN EXECUTION
    #===================================================================
    try {
        if (-not (Initialize-PersistentScript)) { throw "Failed to initialise." }
    
        Start-Transcript -Path $Script:LogFile -Append -Force
    
        $sysInfoHeader = Get-SystemInfo
        $mfg = Get-ManufacturerShareFolder -Manufacturer $sysInfoHeader.RawManufacturer
        $expectedPath = Build-NetworkDriverPath -ManufacturerFolder $mfg -CleanModel $sysInfoHeader.CleanModel
    
        $headerLines = @(
            "======================================================================"
            "       DRIVER UPDATE SCRIPT - PowerShell $($PSVersionTable.PSVersion)"
            "======================================================================"
            "  User:          $env:USERNAME"
            "  Computer:      $env:COMPUTERNAME"
            "  Manufacturer:  $($sysInfoHeader.RawManufacturer)"
            "  Model:         $($sysInfoHeader.RawModel)"
            "  Clean model:   $($sysInfoHeader.CleanModel)"
            "  Driver share:  $expectedPath"
            "  Timeout:       $Script:DriverTimeout sec/driver"
            "  Choco source:  $Script:ChocoSourceName"
            "  Flow:          USB(targeted) -> GuestAuth -> Share(targeted) -> OEM(online)"
            "======================================================================"
        )
        Write-LoggedHost
        foreach ($hl in $headerLines) {
            Write-LoggedHost $hl -ForegroundColor Cyan
        }
        Write-LoggedHost
    
        Write-Status INFO "Checking for orphan processes..."
        $orphanKilled = Stop-DriverInstallProcesses -Silent
        if ($orphanKilled -gt 0) { Write-Status WARN "Terminated $orphanKilled."; Start-Sleep -Seconds 2 }
    
        $state = Get-ScriptState
        Write-Host "--- Current State ---" -ForegroundColor DarkGray
        foreach ($prop in $state.PSObject.Properties) {
            $stLine = "  {0,-35}: {1}" -f $prop.Name, $prop.Value
            Write-Host $stLine -ForegroundColor DarkGray
        }
        Write-Host "--- End State ---" -ForegroundColor DarkGray
        Write-Host ""
    
        # PHASE-0A: USB drivers (TARGETED)
        if ($state.Phase -eq 0) {
            if (-not $state.USBCompleted) {
                $usbResult = Install-USBNetworkDrivers -State $state
                if ($usbResult.NeedsReboot) {
                    $state.USBCompleted = $true; $state.USBRebootDone = $false; Set-ScriptState -State $state
                    Schedule-RebootAndContinue -State $state -Reason "USB drivers - reboot"
                }
                else { $state.USBCompleted = $true; $state.USBRebootDone = $true; Set-ScriptState -State $state }
            }
            else {
                if (-not $state.USBRebootDone) {
                    Write-Status OK "Post-reboot: USB drivers finalized."
                    $state.USBRebootDone = $true; Set-ScriptState -State $state
                }
            }
            # PHASE-0B: Network share drivers (TARGETED)
            if (-not $state.NetworkShareCompleted) {
                $netResult = Install-NetworkShareDrivers -State $state
                if ($netResult.NeedsReboot) {
                    $state.NetworkShareCompleted = $true; $state.NetworkShareRebootDone = $false; Set-ScriptState -State $state
                    Schedule-RebootAndContinue -State $state -Reason "Network share drivers - reboot"
                }
                else { $state.NetworkShareCompleted = $true; $state.NetworkShareRebootDone = $true; $state.Phase = 1; Set-ScriptState -State $state }
            }
            else {
                if (-not $state.NetworkShareRebootDone) {
                    Write-Status OK "Post-reboot: Share drivers finalized."
                    $state.NetworkShareRebootDone = $true
                }
                $state.Phase = 1; Set-ScriptState -State $state
            }
        }
    
        # PHASE-1: Online OEM updates
        if ($state.Phase -ge 1) {
            Write-Banner "PHASE 1: ONLINE OEM UPDATES"
            if (-not (Test-InternetConnectivity -MaxRetries 15 -RetryDelay 10)) {
                Write-Status ERROR "No internet."; try { Stop-Transcript } catch { }; exit 1
            }
            Install-ChocolateyIfNeeded
            $manufacturer = Get-SystemManufacturer
            if ($manufacturer -like '*Dell*') { Invoke-DellUpdates -State $state | Out-Null }
            elseif ($manufacturer -match 'HP|Hewlett[-\s]?Packard') { Invoke-HPUpdates -State $state | Out-Null }
            else { Write-Status WARN "Unsupported: $manufacturer" }
        }
    
        # FINAL SUMMARY
        Stop-DriverInstallProcesses -Silent | Out-Null
        $summaryLines = @(
            "======================================================================"
            "      DRIVER UPDATE SCRIPT COMPLETED SUCCESSFULLY!"
            "======================================================================"
            "  System:          $($state.SystemManufacturer) $($state.SystemModel)"
            "  Clean model:     $($state.CleanModel)"
            "  Share path:      $($state.NetworkSharePath)"
            "  Guest auth:      $($state.GuestAuthConfigured)"
            "  Reboots:         $($state.RebootCount)"
            "  Drivers added:   $($state.TotalDriversAdded)"
            "  Installed:       $($state.TotalDriversInstalled)"
            "  Skipped:         $($state.TotalDriversSkipped)"
            "  Failed:          $($state.TotalDriversFailed)"
            "  Timed out:       $($state.TotalDriversTimedOut)"
            "  Killed:          $($state.TotalProcessesKilled)"
            "  Total time:      $(Format-Duration $state.TotalTimeSpent)"
            "======================================================================"
        )
        Write-LoggedHost
        foreach ($sl in $summaryLines) {
            Write-LoggedHost $sl -ForegroundColor Green
        }
        Write-LoggedHost
        Remove-ScriptState
    }
    catch {
        Write-LoggedHost
        Write-LoggedHost "======================================================================" -ForegroundColor Red
        Write-LoggedHost "  SCRIPT ERROR" -ForegroundColor Red
        Write-LoggedHost "======================================================================" -ForegroundColor Red
        Write-Status ERROR "Message: $($_.Exception.Message)"
        Write-Status ERROR "Line:    $($_.InvocationInfo.ScriptLineNumber)"
        $stackStr = $_.ScriptStackTrace
        Write-Host $stackStr -ForegroundColor Red
        Write-LoggedHost
        Write-Status INFO "Emergency clean-up..."
        try {
            $saveEA = $ErrorActionPreference; $ErrorActionPreference = 'SilentlyContinue'
            Stop-DriverInstallProcesses -Silent | Out-Null
            $ErrorActionPreference = $saveEA
        }
        catch { try { $ErrorActionPreference = $saveEA } catch { } }
        Write-Status INFO "State file kept - re-run to continue."
    }
    finally {
        try { Stop-Transcript } catch { }
    }
    

    Visual example at runtime

    ======================================================================
           DRIVER UPDATE SCRIPT - PowerShell 5.1.19041.4291
    ======================================================================
      Manufacturer:  Dell Inc.
      Model:         Latitude 5400
      Clean model:   latitude5400
      Driver share:  \\networkshare\dell\driver\latitude5400
      Mode:          USB(NIC) -> NetworkShare(all) -> OEM(online)
    ======================================================================
    
    ======================================================================
      PHASE-0A: USB NETWORK DRIVER INSTALLATION
    ======================================================================
    [>] Installing NIC/WiFi drivers from USB to enable network connectivity.
    [+] Found USB driver folder: E:\DriversOS
    [+] Found 3 INF file(s) in: E:\DriversOS
    [i] Device matching DISABLED for USB NETWORK - installing ALL drivers.
    
      ======================================================================
                  USB NETWORK - DRIVER MATCHING RESULTS
      ======================================================================
      Total INF files:              3
      Matched to system devices:    3
      Mode:                         ALL (no filtering)
      ======================================================================
    
      [####################] 100% (3 of 3 drivers)
      Driver: Intel-I219LM\e1d68x64.inf
      [+] Exit code 0 - Success
    
    ======================================================================
      PHASE-0B: NETWORK SHARE DRIVER INSTALLATION
    ======================================================================
    
    --- Identifying System Model ---
    [i] Manufacturer:  Dell Inc.
    [i] Model:         Latitude 5400
    [i] Clean model:   latitude5400
    [i] Share folder:  dell
    
      ======================================================================
                  NETWORK SHARE PATH RESOLUTION
      ======================================================================
      Base:           \\networkshare
      Manufacturer:   dell
      Driver folder:  driver
      Model:          latitude5400
      Full path:      \\networkshare\dell\driver\latitude5400
      ======================================================================
    
    --- Testing Network Share Access ---
    [i] NIC drivers should be installed from USB - testing network...
    [+] Network share accessible: \\networkshare\dell\driver\latitude5400
    
    --- Matching NETWORK SHARE Drivers to System Devices ---
    [+] Scanned 45 device(s), found 312 unique hardware/compatible ID(s).
    [S] No matching device: Chipset\IntelMEI\heci.inf
    [S] No matching device: Chipset\AMT\LMS.inf
    ... (only unmatched shown)
    
      ======================================================================
                  NETWORK SHARE - DRIVER MATCHING RESULTS
      ======================================================================
      Total INF files:              85
      Matched to system devices:    22
      Skipped (no matching device): 63
      Mode:                         TARGETED (matched only)
      ======================================================================
    
    --- Installing 22 NETWORK SHARE Driver(s) ---
      [#---------] 5% (1 of 22 drivers)
      Driver: Video\Intel-UHD620\igdlh64.inf
      ...
    

    Powershell script that searches for the drivers in the USB

    #Requires -RunAsAdministrator
    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'
    
    #===================================================================
    #  CONFIGURATION
    #===================================================================
    $Script:TempFolder       = "C:\Temp"
    $Script:PersistentScript = "C:\Temp\DriverUpdateScript.ps1"
    $Script:StateFile        = "C:\Temp\DriverUpdateState.json"
    $Script:LogFile          = "C:\Temp\DriverUpdateScript.log"
    $Script:RunOnceKey       = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
    $Script:RunOnceName      = 'DriverUpdateScript_RunOnce'
    $Script:DriverTimeout    = 300
    $Script:CooldownSeconds  = 5
    $Script:DriverProcesses  = @('drvinst','pnputil','DPInst','DPInst64','SetupAPI')
    $Script:SpinnerFrames    = @('[ ]','[= ]','[== ]','[=== ]','[====]','[ ===]','[ ==]','[ =]')
    $Script:ChocoSourceName  = 'chocosia'
    $Script:ChocoSourceUrl   = 'http://choco.local.xyz.com/repository/chocolatey-group'
    
    #===================================================================
    #  INITIALISATION
    #===================================================================
    function Initialize-PersistentScript {
        if (-not (Test-Path $Script:TempFolder)) {
            New-Item -Path $Script:TempFolder -ItemType Directory -Force | Out-Null
        }
        $currentScript = $MyInvocation.ScriptName
        if (-not $currentScript) { $currentScript = $PSCommandPath }
        if ($currentScript -and $currentScript -ne $Script:PersistentScript) {
            if (Test-Path $currentScript) {
                Write-Host "[INIT] Copying script to $Script:PersistentScript" -ForegroundColor Yellow
                Copy-Item -Path $currentScript -Destination $Script:PersistentScript -Force
                Write-Host "[INIT] Script copied successfully." -ForegroundColor Green
            }
        }
        if (-not (Test-Path $Script:PersistentScript)) {
            Write-Host "[ERROR] Persistent script could not be created." -ForegroundColor Red
            return $false
        }
        return $true
    }
    
    #===================================================================
    #  LOGGING HELPERS
    #===================================================================
    function Write-Status {
        param(
            [Parameter(Mandatory)]
            [ValidateSet('ERROR','WARN','INFO','STEP','OK','PROGRESS','DEBUG','DRIVER','TIME','SKIP','KILL')]
            [string]$Level,
            [Parameter(Mandatory)][string]$Message
        )
        $timestamp = Get-Date -Format "HH:mm:ss"
        $color = switch ($Level.ToUpper()) {
            'ERROR'    { 'Red' }
            'WARN'     { 'Yellow' }
            'INFO'     { 'Cyan' }
            'STEP'     { 'Magenta' }
            'OK'       { 'Green' }
            'PROGRESS' { 'White' }
            'DEBUG'    { 'DarkGray' }
            'DRIVER'   { 'Blue' }
            'TIME'     { 'DarkYellow' }
            'SKIP'     { 'DarkMagenta' }
            'KILL'     { 'Red' }
            default    { 'White' }
        }
        $prefix = switch ($Level.ToUpper()) {
            'ERROR'    { '[X]' }
            'WARN'     { '[!]' }
            'INFO'     { '[i]' }
            'STEP'     { '[>]' }
            'OK'       { '[+]' }
            'PROGRESS' { '[o]' }
            'DEBUG'    { '[.]' }
            'DRIVER'   { '[D]' }
            'TIME'     { '[T]' }
            'SKIP'     { '[S]' }
            'KILL'     { '[K]' }
            default    { '[ ]' }
        }
        Write-Host "[$timestamp] $prefix $Message" -ForegroundColor $color
    }
    
    function Write-Banner {
        param([string]$Text)
        Write-Host ""
        Write-Host ("=" * 70) -ForegroundColor Magenta
        Write-Host "  $Text" -ForegroundColor Magenta
        Write-Host ("=" * 70) -ForegroundColor Magenta
        Write-Host ""
    }
    
    function Write-SubBanner {
        param([string]$Text)
        Write-Host ""
        Write-Host "--- $Text ---" -ForegroundColor Yellow
        Write-Host ""
    }
    
    function Format-Duration {
        param([double]$Seconds)
        if ($Seconds -lt 60) {
            return "{0:N1}s" -f $Seconds
        }
        elseif ($Seconds -lt 3600) {
            $m = [math]::Floor($Seconds / 60)
            $s = $Seconds % 60
            return "{0}m {1:N0}s" -f $m, $s
        }
        else {
            $h = [math]::Floor($Seconds / 3600)
            $m = [math]::Floor(($Seconds % 3600) / 60)
            return "{0}h {1}m" -f $h, $m
        }
    }
    
    function Get-ConsoleWidth {
        $w = 100
        try {
            $w = [Console]::WindowWidth - 5
            if ($w -lt 50) { $w = 100 }
        }
        catch { }
        return $w
    }
    
    function Write-DriverHeader {
        param([string]$DriverName, [int]$Current, [int]$Total)
        if ($Total -le 0) { $Total = 1 }
        $pct = [math]::Round(($Current / $Total) * 100)
        $filled = [math]::Floor($pct / 5)
        $empty = 20 - $filled
        $bar = "[" + ("#" * $filled) + ("-" * $empty) + "]"
        Write-Host ""
        Write-Host "  ======================================================================" -ForegroundColor DarkCyan
        Write-Host "  $bar $pct% ($Current of $Total drivers)" -ForegroundColor Cyan
        Write-Host "  Driver: $DriverName" -ForegroundColor White
        Write-Host "  Timeout: $Script:DriverTimeout seconds | Monitoring: drvinst.exe" -ForegroundColor DarkGray
        Write-Host "  ======================================================================" -ForegroundColor DarkCyan
    }
    
    #===================================================================
    #  PROCESS-HANDLING HELPERS
    #===================================================================
    function Get-DriverInstallProcesses {
        [System.Collections.ArrayList]$collector = @()
        foreach ($name in $Script:DriverProcesses) {
            $found = $null
            try { $found = Get-Process -Name $name -ErrorAction SilentlyContinue } catch { }
            if ($null -ne $found) {
                foreach ($p in @($found)) { [void]$collector.Add($p) }
            }
        }
        $arr = [object[]]$collector.ToArray()
        return ,$arr
    }
    
    function Stop-DriverInstallProcesses {
        param([switch]$Silent)
        $killed = 0
        foreach ($name in $Script:DriverProcesses) {
            $found = $null
            try { $found = Get-Process -Name $name -ErrorAction SilentlyContinue } catch { }
            if ($null -ne $found) {
                foreach ($proc in @($found)) {
                    if (-not $Silent) {
                        Write-Status KILL "Terminating: $($proc.ProcessName) (PID $($proc.Id))"
                    }
                    try { $proc.CloseMainWindow() | Out-Null } catch { }
                    Start-Sleep -Milliseconds 300
                    try {
                        if (-not $proc.HasExited) {
                            Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
                        }
                    }
                    catch { }
                    $killed++
                }
            }
        }
        foreach ($name in $Script:DriverProcesses) {
            try {
                $saveEA = $ErrorActionPreference
                $ErrorActionPreference = 'SilentlyContinue'
                $null = cmd.exe /c "taskkill /F /IM `"$name.exe`" >nul 2>&1"
                $ErrorActionPreference = $saveEA
            }
            catch {
                try { $ErrorActionPreference = $saveEA } catch { }
            }
        }
        return $killed
    }
    
    function Wait-ForDriverProcesses {
        param([int]$TimeoutSeconds = 30)
        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        while ($sw.Elapsed.TotalSeconds -lt $TimeoutSeconds) {
            $procs = Get-DriverInstallProcesses
            $procCount = ($procs | Measure-Object).Count
            if ($procCount -eq 0) { return $true }
            Start-Sleep -Milliseconds 500
        }
        return $false
    }
    
    function Clear-DriverInstallEnvironment {
        $procs = Get-DriverInstallProcesses
        $procCount = ($procs | Measure-Object).Count
        if ($procCount -gt 0) {
            Write-Status WARN "Found $procCount leftover driver process(es) - cleaning up..."
            $k = Stop-DriverInstallProcesses
            Write-Status INFO "Terminated $k process(es)"
            Start-Sleep -Seconds 2
        }
    }
    
    #===================================================================
    #  VERBOSE COMMAND EXECUTION (real-time stdout/stderr streaming)
    #===================================================================
    function Invoke-CommandVerbose {
        <#
        .SYNOPSIS
            Starts a process, streams stdout/stderr line-by-line with timestamps,
            shows a heartbeat if silent for 30s, saves a full log, returns exit code.
        #>
        param(
            [Parameter(Mandatory)][string]$FilePath,
            [Parameter(Mandatory)][string]$Arguments,
            [Parameter(Mandatory)][string]$Label
        )
    
        if (-not (Test-Path $FilePath)) {
            Write-Status ERROR "Executable not found: $FilePath"
            return 999
        }
    
        $exeName = Split-Path $FilePath -Leaf
        Write-Status INFO "Executing: $exeName $Arguments"
        Write-Host ""
    
        $psi = [System.Diagnostics.ProcessStartInfo]::new()
        $psi.FileName = $FilePath
        $psi.Arguments = $Arguments
        $psi.UseShellExecute = $false
        $psi.RedirectStandardOutput = $true
        $psi.RedirectStandardError = $true
        $psi.CreateNoWindow = $true
    
        $process = [System.Diagnostics.Process]::new()
        $process.StartInfo = $psi
    
        try {
            $null = $process.Start()
        }
        catch {
            Write-Status ERROR "Failed to start $exeName : $($_.Exception.Message)"
            return 999
        }
    
        $sw = [System.Diagnostics.Stopwatch]::StartNew()
        $lastActivity = [System.Diagnostics.Stopwatch]::StartNew()
        $heartbeatSec = 30
        $logLines = [System.Collections.ArrayList]::new()
    
        # Async line readers for stdout and stderr
        $stdoutTask = $process.StandardOutput.ReadLineAsync()
        $stderrTask = $process.StandardError.ReadLineAsync()
        $stdoutDone = $false
        $stderrDone = $false
    
        while ((-not $stdoutDone) -or (-not $stderrDone)) {
            $activity = $false
    
            # --- stdout ---
            if ((-not $stdoutDone) -and $stdoutTask.IsCompleted) {
                $line = $null
                try { $line = $stdoutTask.Result } catch { $stdoutDone = $true; continue }
                if ($null -ne $line) {
                    $trimmed = $line.Trim()
                    if ($trimmed) {
                        $ts = Get-Date -Format 'HH:mm:ss'
                        Write-Host "  [$ts] [$Label] $trimmed" -ForegroundColor White
                        [void]$logLines.Add("[$ts] $trimmed")
                    }
                    $stdoutTask = $process.StandardOutput.ReadLineAsync()
                    $activity = $true
                    $lastActivity.Restart()
                }
                else {
                    $stdoutDone = $true
                }
            }
    
            # --- stderr ---
            if ((-not $stderrDone) -and $stderrTask.IsCompleted) {
                $line = $null
                try { $line = $stderrTask.Result } catch { $stderrDone = $true; continue }
                if ($null -ne $line) {
                    $trimmed = $line.Trim()
                    if ($trimmed) {
                        $ts = Get-Date -Format 'HH:mm:ss'
                        Write-Host "  [$ts] [$Label] $trimmed" -ForegroundColor Yellow
                        [void]$logLines.Add("[$ts] [WARN] $trimmed")
                    }
                    $stderrTask = $process.StandardError.ReadLineAsync()
                    $activity = $true
                    $lastActivity.Restart()
                }
                else {
                    $stderrDone = $true
                }
            }
    
            # --- heartbeat when process is silent ---
            if ((-not $activity) -and ($lastActivity.Elapsed.TotalSeconds -ge $heartbeatSec)) {
                $ts = Get-Date -Format 'HH:mm:ss'
                $elapsed = [math]::Round($sw.Elapsed.TotalSeconds)
                Write-Host "  [$ts] [$Label] ... still running (${elapsed}s elapsed)" -ForegroundColor DarkGray
                [void]$logLines.Add("[$ts] [heartbeat] ${elapsed}s elapsed")
                $lastActivity.Restart()
            }
    
            if (-not $activity) {
                Start-Sleep -Milliseconds 100
            }
        }
    
        $process.WaitForExit()
        $sw.Stop()
        $exitCode = $process.ExitCode
    
        # Save detailed log
        $safeLabel = $Label -replace '[^a-zA-Z0-9]', '_'
        $logFile = Join-Path $Script:TempFolder "${safeLabel}_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
        try {
            $header = "Command : $FilePath $Arguments`nExit    : $exitCode`nDuration: $(Format-Duration $sw.Elapsed.TotalSeconds)`n$('=' * 60)`n"
            $body = ($logLines | ForEach-Object { $_ }) -join "`n"
            "$header$body" | Out-File $logFile -Encoding UTF8 -Force
        }
        catch { }
    
        Write-Host ""
        Write-Status TIME "Completed in $(Format-Duration $sw.Elapsed.TotalSeconds)"
        Write-Status INFO "Exit code: $exitCode"
        Write-Status DEBUG "Log: $logFile"
    
        try { $process.Dispose() } catch { }
        return $exitCode
    }
    
    #===================================================================
    #  CHOCOLATEY PATH HELPER
    #===================================================================
    function Get-ChocoExePath {
        $cmd = Get-Command choco -ErrorAction SilentlyContinue
        if ($null -ne $cmd) { return $cmd.Source }
        $default = "$env:ProgramData\chocolatey\bin\choco.exe"
        if (Test-Path $default) { return $default }
        return $null
    }
    
    #===================================================================
    #  DEVICE SCANNING & INF MATCHING
    #===================================================================
    function Get-SystemDeviceIds {
        $ids = [System.Collections.Generic.HashSet[string]]::new(
            [System.StringComparer]::OrdinalIgnoreCase
        )
        $deviceCount = 0
        $problemDeviceNames = [System.Collections.ArrayList]::new()
        try {
            $devices = @(Get-CimInstance Win32_PnPEntity -ErrorAction Stop)
            $deviceCount = ($devices | Measure-Object).Count
            foreach ($dev in $devices) {
                if ($null -ne $dev.HardwareID) {
                    foreach ($hwid in @($dev.HardwareID)) {
                        if ($hwid) { [void]$ids.Add($hwid) }
                    }
                }
                if ($null -ne $dev.CompatibleID) {
                    foreach ($cid in @($dev.CompatibleID)) {
                        if ($cid) { [void]$ids.Add($cid) }
                    }
                }
                if ($null -ne $dev.ConfigManagerErrorCode -and $dev.ConfigManagerErrorCode -ne 0) {
                    $devName = $dev.Name
                    if (-not $devName) { $devName = $dev.DeviceID }
                    [void]$problemDeviceNames.Add("$devName (error $($dev.ConfigManagerErrorCode))")
                }
            }
        }
        catch {
            Write-Status WARN "WMI device scan failed: $($_.Exception.Message)"
        }
        if ($ids.Count -eq 0) {
            Write-Status INFO "Trying Get-PnpDevice as fallback..."
            try {
                $pnpDevs = @(Get-PnpDevice -ErrorAction SilentlyContinue)
                $deviceCount = ($pnpDevs | Measure-Object).Count
                foreach ($dev in $pnpDevs) {
                    try {
                        $hwProp = $dev | Get-PnpDeviceProperty -KeyName 'DEVPKEY_Device_HardwareIds' -ErrorAction SilentlyContinue
                        if ($null -ne $hwProp -and $null -ne $hwProp.Data) {
                            foreach ($id in @($hwProp.Data)) { if ($id) { [void]$ids.Add($id) } }
                        }
                    }
                    catch { }
                    try {
                        $cProp = $dev | Get-PnpDeviceProperty -KeyName 'DEVPKEY_Device_CompatibleIds' -ErrorAction SilentlyContinue
                        if ($null -ne $cProp -and $null -ne $cProp.Data) {
                            foreach ($id in @($cProp.Data)) { if ($id) { [void]$ids.Add($id) } }
                        }
                    }
                    catch { }
                }
            }
            catch {
                Write-Status WARN "Get-PnpDevice fallback also failed: $($_.Exception.Message)"
            }
        }
        Write-Status OK "Scanned $deviceCount device(s), found $($ids.Count) unique hardware/compatible ID(s)."
        $probCount = ($problemDeviceNames | Measure-Object).Count
        if ($probCount -gt 0) {
            Write-Status WARN "$probCount device(s) with problems (may need drivers):"
            foreach ($pd in $problemDeviceNames) {
                Write-Status INFO "  - $pd"
            }
        }
        else {
            Write-Status OK "No devices with driver problems detected."
        }
        return $ids
    }
    
    function Get-InfHardwareIds {
        param([Parameter(Mandatory)][string]$InfPath)
        $hardwareIds = [System.Collections.Generic.HashSet[string]]::new(
            [System.StringComparer]::OrdinalIgnoreCase
        )
        try { $lines = @(Get-Content $InfPath -ErrorAction Stop) }
        catch { return @($hardwareIds) }
        $lineCount = ($lines | Measure-Object).Count
        if ($lineCount -eq 0) { return @($hardwareIds) }
        $modelSections = [System.Collections.Generic.HashSet[string]]::new(
            [System.StringComparer]::OrdinalIgnoreCase
        )
        $inManufacturer = $false
        foreach ($line in $lines) {
            $trimmed = $line.Trim()
            if ($trimmed.StartsWith(';')) { continue }
            if ($trimmed -match '^\[Manufacturer\]\s*$') { $inManufacturer = $true; continue }
            if ($inManufacturer) {
                if ($trimmed -match '^\[') { $inManufacturer = $false; continue }
                if (-not $trimmed) { continue }
                if ($trimmed -match '=') {
                    $rightSide = ($trimmed -split '=', 2)[1]
                    if ($null -eq $rightSide) { continue }
                    $parts = $rightSide -split ','
                    $baseName = $parts[0].Trim()
                    if (-not $baseName) { continue }
                    [void]$modelSections.Add($baseName)
                    $partCount = ($parts | Measure-Object).Count
                    for ($j = 1; $j -lt $partCount; $j++) {
                        $decoration = $parts[$j].Trim()
                        if ($decoration) { [void]$modelSections.Add("$baseName.$decoration") }
                    }
                }
            }
        }
        foreach ($section in @($modelSections)) {
            $sectionHeader = "[$section]"
            $inSection = $false
            foreach ($line in $lines) {
                $trimmed = $line.Trim()
                if ($trimmed.StartsWith(';')) { continue }
                if ($trimmed -ieq $sectionHeader) { $inSection = $true; continue }
                if ($inSection) {
                    if ($trimmed -match '^\[') { $inSection = $false; continue }
                    if (-not $trimmed) { continue }
                    if ($trimmed -match '=') {
                        $rightSide = ($trimmed -split '=', 2)[1]
                        if ($null -eq $rightSide) { continue }
                        $idParts = $rightSide -split ','
                        $idPartCount = ($idParts | Measure-Object).Count
                        for ($j = 1; $j -lt $idPartCount; $j++) {
                            $hwid = $idParts[$j].Trim().Trim('"').Trim()
                            if ($hwid -and $hwid -match '\\') { [void]$hardwareIds.Add($hwid) }
                        }
                    }
                }
            }
        }
        return @($hardwareIds)
    }
    
    function Test-InfMatchesSystem {
        param(
            [Parameter(Mandatory)][string]$InfPath,
            [Parameter(Mandatory)][System.Collections.Generic.HashSet[string]]$SystemIds
        )
        $infIds = Get-InfHardwareIds -InfPath $InfPath
        $infIdCount = ($infIds | Measure-Object).Count
        if ($infIdCount -eq 0) { return $true }
        foreach ($infId in $infIds) {
            if ($SystemIds.Contains($infId)) { return $true }
        }
        return $false
    }
    
    #===================================================================
    #  STATE-MANAGEMENT HELPERS
    #===================================================================
    function Get-ScriptState {
        if (Test-Path $Script:StateFile) {
            try {
                $json = Get-Content $Script:StateFile -Raw -ErrorAction Stop | ConvertFrom-Json
                $defaults = @{
                    Phase = 0; VentoyCompleted = $false; VentoyRebootDone = $false
                    VentoyProcessedDrivers = @(); VentoyDriverRoot = ""
                    ManufacturerPhase = 0; DellADRFailed = $false; LastExitCode = 0
                    RebootCount = 0; TotalDriversAdded = 0; TotalDriversInstalled = 0
                    TotalDriversFailed = 0; TotalDriversSkipped = 0
                    TotalDriversTimedOut = 0; TotalTimeSpent = 0; TotalProcessesKilled = 0
                }
                foreach ($k in $defaults.Keys) {
                    if (-not $json.PSObject.Properties[$k]) {
                        $json | Add-Member -NotePropertyName $k -NotePropertyValue $defaults[$k] -Force
                    }
                }
                return $json
            }
            catch {
                Write-Status WARN "Failed reading state file - starting fresh: $($_.Exception.Message)"
            }
        }
        return [PSCustomObject]@{
            Phase = 0; VentoyCompleted = $false; VentoyRebootDone = $false
            VentoyProcessedDrivers = @(); VentoyDriverRoot = ""
            ManufacturerPhase = 0; DellADRFailed = $false; LastExitCode = 0
            RebootCount = 0; TotalDriversAdded = 0; TotalDriversInstalled = 0
            TotalDriversFailed = 0; TotalDriversSkipped = 0
            TotalDriversTimedOut = 0; TotalTimeSpent = 0; TotalProcessesKilled = 0
        }
    }
    
    function Set-ScriptState {
        param([pscustomobject]$State)
        $State | ConvertTo-Json -Depth 10 | Out-File $Script:StateFile -Encoding UTF8 -Force
    }
    
    function Remove-ScriptState {
        Write-Status INFO "Removing state file and RunOnce entry..."
        if (Test-Path $Script:StateFile) { Remove-Item $Script:StateFile -Force -ErrorAction SilentlyContinue }
        try {
            $rk64 = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
                [Microsoft.Win32.RegistryHive]::LocalMachine,
                [Microsoft.Win32.RegistryView]::Registry64
            ).OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', $true)
            if ($null -ne $rk64) { $rk64.DeleteValue($Script:RunOnceName, $false); $rk64.Close() }
        }
        catch { }
        $fallback = "$($Script:RunOnceName)_Task"
        try {
            $existingTask = Get-ScheduledTask -TaskName $fallback -ErrorAction SilentlyContinue
            if ($null -ne $existingTask) {
                Unregister-ScheduledTask -TaskName $fallback -Confirm:$false -ErrorAction SilentlyContinue
                Write-Status INFO "Deleted fallback Scheduled-Task: $fallback"
            }
        }
        catch { }
        Write-Status OK "Cleanup finished."
    }
    
    #===================================================================
    #  RUNONCE HELPERS
    #===================================================================
    function Set-RunOnceEntry {
        param([Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$Command)
        try {
            $rk64 = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
                [Microsoft.Win32.RegistryHive]::LocalMachine,
                [Microsoft.Win32.RegistryView]::Registry64
            ).OpenSubKey('SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce', $true)
            $rk64.SetValue($Name, $Command, [Microsoft.Win32.RegistryValueKind]::String)
            $rk64.Close()
            Write-Status OK "RunOnce entry created (64-bit): $Name"
            return $true
        }
        catch {
            Write-Status WARN "Failed to write RunOnce (64-bit): $($_.Exception.Message)"
            return $false
        }
    }
    
    function Set-RunOnceTask {
        param([Parameter(Mandatory)][string]$Name, [Parameter(Mandatory)][string]$ScriptPath)
        $taskAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoLogo -NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
        $taskTrigger = New-ScheduledTaskTrigger -AtStartup
        $principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest -LogonType ServiceAccount
        try {
            Register-ScheduledTask -TaskName $Name -Action $taskAction -Trigger $taskTrigger -Principal $principal -Force -ErrorAction Stop
            Write-Status OK "Fallback Scheduled-Task created: $Name"
            return $true
        }
        catch {
            Write-Status ERROR "Could not create fallback Scheduled-Task: $($_.Exception.Message)"
            return $false
        }
    }
    
    function Schedule-RebootAndContinue {
        param([pscustomobject]$State, [string]$Reason = "Script requires reboot to continue")
        Write-Host ""
        Write-Host "======================================================================" -ForegroundColor Yellow
        Write-Host "  REBOOT REQUIRED" -ForegroundColor Yellow
        Write-Host "======================================================================" -ForegroundColor Yellow
        Write-Host "  Reason: $Reason" -ForegroundColor Yellow
        Write-Host "  The script will automatically resume after reboot." -ForegroundColor Yellow
        Write-Host "======================================================================" -ForegroundColor Yellow
        Write-Host ""
        Write-Status INFO "Cleaning up driver processes before reboot..."
        Stop-DriverInstallProcesses -Silent | Out-Null
        $State.RebootCount++
        Set-ScriptState -State $State
        $cmd = "powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$Script:PersistentScript`""
        $runOnceCreated = Set-RunOnceEntry -Name $Script:RunOnceName -Command $cmd
        if (-not $runOnceCreated) {
            $fallbackName = "$($Script:RunOnceName)_Task"
            $taskCreated = Set-RunOnceTask -Name $fallbackName -ScriptPath $Script:PersistentScript
            if (-not $taskCreated) {
                Write-Status ERROR "Unable to schedule script after reboot - manual restart required."
            }
        }
        Write-Status INFO "Reboot scheduled - system will restart in 15 seconds..."
        for ($i = 15; $i -ge 1; $i--) {
            Write-Host "  Rebooting in $i seconds..." -ForegroundColor Yellow
            Start-Sleep -Seconds 1
        }
        Write-Host ""
        Write-Host "  Rebooting NOW!" -ForegroundColor Red
        Write-Host ""
        try { Stop-Transcript } catch { }
        Restart-Computer -Force
        exit 0
    }
    
    #===================================================================
    #  DRIVER INSTALLATION (single driver with timeout & monitoring)
    #===================================================================
    function Install-SingleDriver {
        param(
            [Parameter(Mandatory)][string]$InfPath,
            [Parameter(Mandatory)][string]$DriverName,
            [Parameter(Mandatory)][int]$CurrentNumber,
            [Parameter(Mandatory)][int]$TotalCount,
            [int]$TimeoutSeconds = $Script:DriverTimeout
        )
        $result = [PSCustomObject]@{
            InfPath = $InfPath; DriverName = $DriverName; Success = $false
            ExitCode = -1; Added = $false; Installed = $false; AlreadyExists = $false
            NeedsReboot = $false; TimedOut = $false; ProcessesKilled = 0
            ErrorMessage = ""; Output = ""; Duration = 0
        }
        Write-DriverHeader -DriverName $DriverName -Current $CurrentNumber -Total $TotalCount
        Write-Status DRIVER "Path: $InfPath"
        Clear-DriverInstallEnvironment
        Write-Status INFO "Starting installation with process monitoring..."
        Write-Host ""
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        $consoleWidth = Get-ConsoleWidth
        $tempScript = [IO.Path]::GetTempFileName() + ".ps1"
        $outFile = [IO.Path]::GetTempFileName()
        $exitFile = [IO.Path]::GetTempFileName()
        $wrapperContent = @"
    try {
        `$out = & pnputil.exe /add-driver "$InfPath" /install 2>&1 | Out-String
        `$out | Out-File -FilePath "$outFile" -Encoding UTF8
        `$LASTEXITCODE | Out-File -FilePath "$exitFile" -Encoding UTF8
    } catch {
        `$_.Exception.Message | Out-File -FilePath "$outFile" -Encoding UTF8
        "999" | Out-File -FilePath "$exitFile" -Encoding UTF8
    }
    "@
        $wrapperContent | Out-File -FilePath $tempScript -Encoding UTF8
        $proc = Start-Process -FilePath "powershell.exe" -ArgumentList "-ExecutionPolicy Bypass -WindowStyle Hidden -File `"$tempScript`"" -PassThru -WindowStyle Hidden
        $frameIdx = 0
        $frameCount = $Script:SpinnerFrames.Length
        $animLine = 0
        try { $animLine = [Console]::CursorTop } catch { }
        while ((-not $proc.HasExited) -and ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds)) {
            $elapsed = $stopwatch.Elapsed.TotalSeconds
            $remaining = [math]::Max(0, $TimeoutSeconds - $elapsed)
            $pct = [math]::Round(($elapsed / $TimeoutSeconds) * 100)
            $spinner = $Script:SpinnerFrames[$frameIdx % $frameCount]
            $frameIdx++
            $line = "  $spinner $pct% | Elapsed: $([math]::Round($elapsed))s | Remaining: $([math]::Round($remaining))s"
            $padded = $line.PadRight($consoleWidth)
            try {
                [Console]::SetCursorPosition(0, $animLine)
                [Console]::ForegroundColor = [ConsoleColor]::Cyan
                [Console]::Write($padded)
                [Console]::ResetColor()
            }
            catch { Write-Host "`r$padded" -NoNewline -ForegroundColor Cyan }
            Start-Sleep -Milliseconds 150
        }
        try {
            [Console]::SetCursorPosition(0, $animLine)
            [Console]::Write(' ' * $consoleWidth)
            [Console]::SetCursorPosition(0, $animLine)
        }
        catch { Write-Host "`r$(' ' * $consoleWidth)`r" -NoNewline }
        $stopwatch.Stop()
        $result.Duration = $stopwatch.Elapsed.TotalSeconds
        if (-not $proc.HasExited) {
            Write-Host ""
            Write-Status SKIP "TIMEOUT after $(Format-Duration $TimeoutSeconds)!"
            try { $proc.Kill() } catch { }
            $result.TimedOut = $true
            $result.ErrorMessage = "Timeout after $TimeoutSeconds seconds"
            $killed = Stop-DriverInstallProcesses
            $result.ProcessesKilled = $killed
            Write-Status KILL "Terminated $killed driver-process(es)"
            $null = Wait-ForDriverProcesses -TimeoutSeconds 10
            Remove-Item $tempScript, $outFile, $exitFile -Force -ErrorAction SilentlyContinue
            Write-Status TIME "Time spent: $(Format-Duration $result.Duration) (TIMEOUT)"
            if ($Script:CooldownSeconds -gt 0) { Start-Sleep -Seconds $Script:CooldownSeconds }
            Write-Host ""
            return $result
        }
        $out = ""
        $exit = -1
        if (Test-Path $outFile) { $out = Get-Content $outFile -Raw -ErrorAction SilentlyContinue }
        if (Test-Path $exitFile) {
            $raw = Get-Content $exitFile -Raw -ErrorAction SilentlyContinue
            if ($null -ne $raw) { [int]::TryParse($raw.Trim(), [ref]$exit) | Out-Null }
        }
        Remove-Item $tempScript, $outFile, $exitFile -Force -ErrorAction SilentlyContinue
        $result.ExitCode = $exit
        $result.Output = $out
        if ($null -ne $out) {
            if ($out -match 'added|successfully added|driver package added') {
                $result.Added = $true; Write-Status OK "Driver added to store"
            }
            if ($out -match 'install.*device|installed to device') {
                $result.Installed = $true; Write-Status OK "Driver installed on device"
            }
            if ($out -match 'already exists|up to date') {
                $result.AlreadyExists = $true; Write-Status INFO "Driver already up-to-date"
            }
            if ($out -match 'reboot|restart') {
                $result.NeedsReboot = $true; Write-Status WARN "Reboot required (output)"
            }
        }
        switch ($exit) {
            0    { $result.Success = $true; Write-Status OK "Exit code 0 - Success" }
            1    { $result.Success = $true; Write-Status INFO "Exit code 1 - Success with warnings" }
            259  { $result.Success = $true; Write-Status INFO "Exit code 259 - staged (no matching device)" }
            3010 { $result.Success = $true; $result.NeedsReboot = $true; Write-Status WARN "Exit code 3010 - Reboot required" }
            482  { $result.Success = $true; $result.NeedsReboot = $true; Write-Status WARN "Exit code 482 - Partial success, reboot needed" }
            default {
                if ($exit -ge 0) {
                    if ($result.Added -or $result.Installed -or $result.AlreadyExists) {
                        $result.Success = $true; Write-Status INFO "Exit code $exit - driver processed"
                    }
                    else {
                        $result.Success = $false; $result.ErrorMessage = "Exit code $exit"
                        Write-Status ERROR "Exit code $exit - failure"
                    }
                }
                else { $result.Success = $true; Write-Status WARN "Unable to determine exit code" }
            }
        }
        Write-Status TIME "Time taken: $(Format-Duration $result.Duration)"
        if ($Script:CooldownSeconds -gt 0) { Start-Sleep -Seconds $Script:CooldownSeconds }
        return $result
    }
    
    #===================================================================
    #  PHASE-0: VENTOY OFFLINE DRIVER INSTALLATION
    #===================================================================
    function Install-VentoyDrivers {
        param([string[]]$FolderCandidates = @('DriversOS','Drivers'), [pscustomobject]$State)
        Write-Banner "PHASE-0: VENTOY OFFLINE DRIVER INSTALLATION"
        Write-Status INFO "Performing initial cleanup..."
        Clear-DriverInstallEnvironment
        $log = Join-Path $env:WINDIR 'Temp\InstallVentoyDrivers.log'
        "=== Install-VentoyDrivers start $(Get-Date) ===" | Out-File $log -Encoding UTF8 -Append
        $result = [PSCustomObject]@{
            Success = $false; NeedsReboot = $false; NotFound = $false
            TotalFound = 0; TotalMatched = 0; TotalSkippedNoMatch = 0
            TotalAdded = 0; TotalInstalled = 0; TotalFailed = 0; TotalSkipped = 0
            TotalAlreadyExist = 0; TotalTimedOut = 0; TotalTime = 0; TotalKilled = 0
        }
        $root = $null
        if ($State.VentoyDriverRoot -and (Test-Path $State.VentoyDriverRoot)) {
            $root = $State.VentoyDriverRoot
            Write-Status OK "Using saved driver root: $root"
        }
        else {
            $driveLetters = @('D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z')
            foreach ($drive in $driveLetters) {
                foreach ($folder in $FolderCandidates) {
                    $candidate = "${drive}:\$folder"
                    if (Test-Path $candidate) { $root = $candidate; Write-Status OK "Found driver folder: $root"; break }
                }
                if ($null -ne $root) { break }
            }
        }
        if ($null -eq $root) {
            Write-Status INFO "No Ventoy driver folder found."
            $result.NotFound = $true; $result.Success = $true; return $result
        }
        $State.VentoyDriverRoot = $root; Set-ScriptState -State $State
        $allInfFiles = @(Get-ChildItem -Path $root -Filter "*.inf" -Recurse -File -ErrorAction SilentlyContinue | Sort-Object FullName)
        $totalFound = ($allInfFiles | Measure-Object).Count
        if ($totalFound -eq 0) {
            Write-Status WARN "No .inf files discovered."
            $result.NotFound = $true; $result.Success = $true; return $result
        }
        $result.TotalFound = $totalFound
        Write-Status OK "Found $totalFound INF file(s) in driver folder."
        $processed = @()
        if ($State.VentoyProcessedDrivers) { $processed = @($State.VentoyProcessedDrivers) }
        $processedCount = ($processed | Measure-Object).Count
        if ($processedCount -gt 0) { Write-Status INFO "$processedCount driver(s) already processed." }
        Write-SubBanner "Scanning System Devices"
        $scanSw = [System.Diagnostics.Stopwatch]::StartNew()
        $systemIds = Get-SystemDeviceIds
        $scanSw.Stop()
        Write-Status TIME "Device scan took $(Format-Duration $scanSw.Elapsed.TotalSeconds)"
        Write-SubBanner "Matching Drivers to System Devices"
        $matchSw = [System.Diagnostics.Stopwatch]::StartNew()
        $matchedInfs = [System.Collections.ArrayList]::new()
        $skippedNoMatch = [System.Collections.ArrayList]::new()
        $fallbackMode = $false
        if ($systemIds.Count -eq 0) {
            Write-Status WARN "Could not enumerate any device IDs - installing ALL drivers as fallback."
            $fallbackMode = $true
            foreach ($inf in $allInfFiles) { [void]$matchedInfs.Add($inf) }
        }
        else {
            foreach ($inf in $allInfFiles) {
                $rel = $inf.FullName.Substring($root.Length + 1)
                if ($rel -in $processed) { continue }
                $isMatch = Test-InfMatchesSystem -InfPath $inf.FullName -SystemIds $systemIds
                if ($isMatch) { [void]$matchedInfs.Add($inf) }
                else { [void]$skippedNoMatch.Add($inf); Write-Status SKIP "No matching device: $rel" }
            }
        }
        $matchSw.Stop()
        $matchedCount = ($matchedInfs | Measure-Object).Count
        $skippedNoMatchCount = ($skippedNoMatch | Measure-Object).Count
        $result.TotalMatched = $matchedCount
        $result.TotalSkippedNoMatch = $skippedNoMatchCount
        Write-Host ""
        Write-Host "  ======================================================================" -ForegroundColor DarkCyan
        Write-Host "                     DRIVER MATCHING RESULTS" -ForegroundColor Cyan
        Write-Host "  ======================================================================" -ForegroundColor DarkCyan
        Write-Host "  Total INF files found:        $totalFound" -ForegroundColor White
        Write-Host "  Already processed:            $processedCount" -ForegroundColor DarkGray
        Write-Host "  Matched to system devices:    $matchedCount" -ForegroundColor Green
        Write-Host "  Skipped (no matching device): $skippedNoMatchCount" -ForegroundColor Yellow
        Write-Host "  Matching time:                $(Format-Duration $matchSw.Elapsed.TotalSeconds)" -ForegroundColor DarkGray
        $modeLabel = if ($fallbackMode) { "FALLBACK (all drivers)" } else { "TARGETED (matched only)" }
        $modeColor = if ($fallbackMode) { 'Red' } else { 'Green' }
        Write-Host "  Mode:                         $modeLabel" -ForegroundColor $modeColor
        Write-Host "  ======================================================================" -ForegroundColor DarkCyan
        Write-Host ""
        if ($matchedCount -eq 0) {
            Write-Status OK "No new drivers to install."
            $result.Success = $true; return $result
        }
        $overallSw = [System.Diagnostics.Stopwatch]::StartNew()
        $globalReboot = $false
        $i = 0
        Write-SubBanner "Installing $matchedCount Matched Driver(s)"
        foreach ($inf in $matchedInfs) {
            $i++
            $rel = $inf.FullName.Substring($root.Length + 1)
            if ($rel -in $processed) { continue }
            $drvResult = Install-SingleDriver -InfPath $inf.FullName -DriverName $rel -CurrentNumber $i -TotalCount $matchedCount
            $result.TotalTime += $drvResult.Duration
            $result.TotalKilled += $drvResult.ProcessesKilled
            if ($drvResult.TimedOut) { $result.TotalTimedOut++ }
            elseif ($drvResult.Added) { $result.TotalAdded++ }
            if ($drvResult.Installed) { $result.TotalInstalled++ }
            if ($drvResult.AlreadyExists) { $result.TotalAlreadyExist++ }
            if ((-not $drvResult.Success) -and (-not $drvResult.TimedOut)) { $result.TotalFailed++ }
            if ($drvResult.NeedsReboot) { $globalReboot = $true }
            $processed += $rel
            $State.VentoyProcessedDrivers = $processed
            $State.TotalDriversAdded = $result.TotalAdded
            $State.TotalDriversInstalled = $result.TotalInstalled
            $State.TotalDriversFailed = $result.TotalFailed
            $State.TotalDriversSkipped = $result.TotalSkippedNoMatch
            $State.TotalDriversTimedOut = $result.TotalTimedOut
            $State.TotalTimeSpent = $result.TotalTime
            $State.TotalProcessesKilled = $result.TotalKilled
            Set-ScriptState -State $State
        }
        $overallSw.Stop()
        $failColor = if ($result.TotalFailed -gt 0) { 'Red' } else { 'Green' }
        $timeoutColor = if ($result.TotalTimedOut -gt 0) { 'Yellow' } else { 'Green' }
        $killColor = if ($result.TotalKilled -gt 0) { 'Yellow' } else { 'Green' }
        $rebootColor = if ($globalReboot) { 'Yellow' } else { 'Green' }
        Write-Host ""
        Write-Host "======================================================================" -ForegroundColor Green
        Write-Host "               VENTOY DRIVER INSTALLATION SUMMARY" -ForegroundColor Green
        Write-Host "======================================================================" -ForegroundColor Green
        Write-Host "  Total INFs found:       $($result.TotalFound)" -ForegroundColor White
        Write-Host "  Matched to devices:     $($result.TotalMatched)" -ForegroundColor Green
        Write-Host "  Skipped (no match):     $($result.TotalSkippedNoMatch)" -ForegroundColor Yellow
        Write-Host "  Added to store:         $($result.TotalAdded)" -ForegroundColor Green
        Write-Host "  Installed on hardware:  $($result.TotalInstalled)" -ForegroundColor Green
        Write-Host "  Already up-to-date:     $($result.TotalAlreadyExist)" -ForegroundColor Cyan
        Write-Host "  Failed:                 $($result.TotalFailed)" -ForegroundColor $failColor
        Write-Host "  Timed out:              $($result.TotalTimedOut)" -ForegroundColor $timeoutColor
        Write-Host "  Processes killed:       $($result.TotalKilled)" -ForegroundColor $killColor
        Write-Host "  Total install time:     $(Format-Duration $overallSw.Elapsed.TotalSeconds)" -ForegroundColor Magenta
        Write-Host "  Reboot required:        $globalReboot" -ForegroundColor $rebootColor
        Write-Host "======================================================================" -ForegroundColor Green
        Write-Host ""
        $result.Success = $true
        $result.NeedsReboot = $globalReboot
        return $result
    }
    
    #===================================================================
    #  INTERNET CONNECTIVITY CHECK
    #===================================================================
    function Test-InternetConnectivity {
        param([int]$MaxRetries = 15, [int]$RetryDelay = 10)
        Write-Banner "CHECKING INTERNET CONNECTIVITY"
        $w = Get-ConsoleWidth
        $totalFrames = $Script:SpinnerFrames.Length
        for ($i = 1; $i -le $MaxRetries; $i++) {
            Write-Status PROGRESS "Attempt $i of $MaxRetries..."
            try {
                $r = Invoke-WebRequest -Uri "https://www.google.com" -Method Head -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
                if ($r.StatusCode -eq 200) { Write-Status OK "Internet reachable."; return $true }
            }
            catch {
                Write-Status WARN "Attempt $i failed: $($_.Exception.Message)"
                if ($i -lt $MaxRetries) {
                    Write-Status INFO "Retrying in $RetryDelay seconds..."
                    for ($s = $RetryDelay; $s -ge 1; $s--) {
                        $spinner = $Script:SpinnerFrames[($RetryDelay - $s) % $totalFrames]
                        $line = "  $spinner Waiting $s sec..."
                        $pad = $line.PadRight($w)
                        try {
                            [Console]::SetCursorPosition(0, [Console]::CursorTop)
                            [Console]::ForegroundColor = [ConsoleColor]::DarkGray
                            [Console]::Write($pad)
                            [Console]::ResetColor()
                        }
                        catch { Write-Host "`r$pad" -NoNewline -ForegroundColor DarkGray }
                        Start-Sleep -Seconds 1
                    }
                }
            }
        }
        Write-Status ERROR "No internet after $MaxRetries attempts."
        return $false
    }
    
    #===================================================================
    #  MANUFACTURER DETECTION
    #===================================================================
    function Get-SystemManufacturer {
        Write-SubBanner "Detecting System Manufacturer"
        $cs = Get-CimInstance Win32_ComputerSystem
        $man = $cs.Manufacturer.Trim()
        $mod = $cs.Model.Trim()
        Write-Status INFO "Manufacturer: $man"
        Write-Status INFO "Model:        $mod"
        return $man
    }
    
    #===================================================================
    #  CHOCOLATEY INSTALL + SOURCE CONFIGURATION
    #===================================================================
    function Set-ChocolateySource {
        Write-SubBanner "Configuring Chocolatey Sources"
        $chocoExe = Get-ChocoExePath
        if (-not $chocoExe) {
            Write-Status ERROR "Chocolatey executable not found - cannot configure sources."
            return
        }
    
        # Check if source exists
        Write-Status INFO "Checking existing Chocolatey sources..."
        $listExit = Invoke-CommandVerbose -FilePath $chocoExe -Arguments "source list" -Label "CHOCO"
    
        # Try adding the custom source
        Write-Status INFO "Adding custom source: $($Script:ChocoSourceName) -> $($Script:ChocoSourceUrl)"
        $addExit = Invoke-CommandVerbose -FilePath $chocoExe -Arguments "source add -n `"$($Script:ChocoSourceName)`" -s `"$($Script:ChocoSourceUrl)`" --priority=1 --allow-self-service" -Label "CHOCO"
        if ($addExit -eq 0) {
            Write-Status OK "Custom source '$($Script:ChocoSourceName)' configured."
        }
        else {
            Write-Status WARN "Source add returned exit code $addExit (may already exist - OK)."
        }
    
        # Disable default community source
        Write-Status INFO "Disabling default Chocolatey community source..."
        $disableExit = Invoke-CommandVerbose -FilePath $chocoExe -Arguments "source disable -n `"chocolatey`"" -Label "CHOCO"
        if ($disableExit -eq 0) {
            Write-Status OK "Default community source disabled."
        }
        else {
            Write-Status WARN "Could not disable default source (may not exist)."
        }
    
        # Verify final state
        Write-Status INFO "Final source configuration:"
        $null = Invoke-CommandVerbose -FilePath $chocoExe -Arguments "source list" -Label "CHOCO"
    }
    
    function Install-ChocolateyIfNeeded {
        Write-SubBanner "Ensuring Chocolatey"
        if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
            Write-Status INFO "Chocolatey not present - installing..."
            Write-Host ""
            Set-ExecutionPolicy Bypass -Scope Process -Force
            [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072
            Invoke-Expression (Invoke-RestMethod https://community.chocolatey.org/install.ps1)
            $env:Path = [System.Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('Path', 'User')
            Write-Host ""
            Write-Status OK "Chocolatey installed."
        }
        else {
            Write-Status OK "Chocolatey already installed."
        }
        Set-ChocolateySource
    }
    
    function Install-ChocoPackage {
        <#
        .SYNOPSIS
            Installs a chocolatey package using Invoke-CommandVerbose so every line
            of choco output (download progress, install steps, etc.) is visible.
        #>
        param(
            [Parameter(Mandatory)][string]$PackageName,
            [string]$DisplayName
        )
        if (-not $DisplayName) { $DisplayName = $PackageName }
        $chocoExe = Get-ChocoExePath
        if (-not $chocoExe) {
            Write-Status ERROR "Chocolatey executable not found."
            return $false
        }
    
        Write-SubBanner "Installing $DisplayName via Chocolatey"
        Write-Status INFO "Source: $($Script:ChocoSourceName) ($($Script:ChocoSourceUrl))"
        Write-Host ""
    
        # Try custom source first
        $chocoArgs = "install $PackageName -y --source=`"$($Script:ChocoSourceName)`""
        $exitCode = Invoke-CommandVerbose -FilePath $chocoExe -Arguments $chocoArgs -Label "CHOCO"
    
        if ($exitCode -ne 0) {
            Write-Status WARN "Install from custom source returned $exitCode - trying default sources..."
            $chocoArgs = "install $PackageName -y"
            $exitCode = Invoke-CommandVerbose -FilePath $chocoExe -Arguments $chocoArgs -Label "CHOCO"
        }
    
        if ($exitCode -eq 0) {
            Write-Status OK "$DisplayName installed successfully."
            return $true
        }
        else {
            Write-Status ERROR "$DisplayName installation failed (exit $exitCode)."
            return $false
        }
    }
    
    #===================================================================
    #  DELL UPDATE LOGIC  (fully verbose - no spinners)
    #===================================================================
    function Invoke-DellUpdates {
        param([pscustomobject]$State)
        Write-Banner "DELL SYSTEM UPDATE"
    
        # Locate dcu-cli.exe
        $cli = @(
            'C:\Program Files\Dell\CommandUpdate\dcu-cli.exe',
            'C:\Program Files (x86)\Dell\CommandUpdate\dcu-cli.exe'
        ) | Where-Object { Test-Path $_ } | Select-Object -First 1
    
        if (-not $cli) {
            $installed = Install-ChocoPackage -PackageName "dellcommandupdate" -DisplayName "Dell Command Update"
            if ($installed) { Start-Sleep -Seconds 5 }
            $cli = @(
                'C:\Program Files\Dell\CommandUpdate\dcu-cli.exe',
                'C:\Program Files (x86)\Dell\CommandUpdate\dcu-cli.exe'
            ) | Where-Object { Test-Path $_ } | Select-Object -First 1
        }
    
        if (-not $cli) {
            Write-Status ERROR "Dell Command Update CLI still missing - aborting Dell section."
            return $false
        }
    
        Write-Status OK "Dell CLI located: $cli"
        Write-Host ""
    
        switch ($State.ManufacturerPhase) {
            0 {
                # --- Phase-0: Advanced Driver Restore ---
                if ($State.DellADRFailed) {
                    Write-Status WARN "ADR previously failed - skipping to phase-2."
                    $State.ManufacturerPhase = 2
                    Set-ScriptState -State $State
                    return (Invoke-DellUpdates -State $State)
                }
    
                Write-SubBanner "Dell Phase-0: Configuring ADR"
                Write-Status INFO "Enabling Advanced Driver Restore..."
                $configExit = Invoke-CommandVerbose -FilePath $cli -Arguments "/configure -advancedDriverRestore=enable" -Label "DCU-CFG"
                Write-Host ""
    
                Write-SubBanner "Dell Phase-0: ADR Driver Install"
                Write-Status STEP "This will download and install drivers matching your Dell system."
                Write-Status STEP "All progress and driver names will be displayed below."
                Write-Host ""
                $dellExit = Invoke-CommandVerbose -FilePath $cli -Arguments "/driverinstall" -Label "DCU"
    
                switch ($dellExit) {
                    0 {
                        Write-Status OK "ADR driver install succeeded."
                        $State.ManufacturerPhase = 2
                        Set-ScriptState -State $State
                        return (Invoke-DellUpdates -State $State)
                    }
                    1 {
                        Write-Status WARN "Drivers installed - reboot required."
                        $State.ManufacturerPhase = 1
                        Schedule-RebootAndContinue -State $State -Reason "Dell ADR driver install (exit 1) requires reboot"
                    }
                    5 {
                        Write-Status WARN "Reboot required to complete operation."
                        $State.ManufacturerPhase = 1
                        Schedule-RebootAndContinue -State $State -Reason "Dell ADR driver install (exit 5) requires reboot"
                    }
                    2 {
                        Write-Status ERROR "ADR crashed - marking as failed, skipping to updates."
                        $State.DellADRFailed = $true
                        $State.ManufacturerPhase = 2
                        Set-ScriptState -State $State
                        return (Invoke-DellUpdates -State $State)
                    }
                    3 {
                        Write-Status OK "All drivers already up to date."
                        $State.ManufacturerPhase = 2
                        Set-ScriptState -State $State
                        return (Invoke-DellUpdates -State $State)
                    }
                    default {
                        Write-Status ERROR "ADR returned $dellExit - skipping to phase-2."
                        $State.DellADRFailed = $true
                        $State.ManufacturerPhase = 2
                        Set-ScriptState -State $State
                        return (Invoke-DellUpdates -State $State)
                    }
                }
            }
            1 {
                # --- Phase-1: Post-reboot scan ---
                Write-SubBanner "Dell Phase-1: Post-Reboot Scan"
                Write-Status INFO "Scanning for remaining Dell drivers after reboot..."
                Write-Host ""
                $null = Invoke-CommandVerbose -FilePath $cli -Arguments "/scan" -Label "DCU"
                $State.ManufacturerPhase = 2
                Set-ScriptState -State $State
                return (Invoke-DellUpdates -State $State)
            }
            2 {
                # --- Phase-2: Apply all updates ---
                Write-SubBanner "Dell Phase-2: Apply BIOS / Firmware / Driver Updates"
                Write-Status STEP "Downloading and installing all available Dell updates."
                Write-Status STEP "Each update name, download progress, and install status shown below."
                Write-Host ""
                $dellExit = Invoke-CommandVerbose -FilePath $cli -Arguments "/applyupdates -forceUpdate=enable -autoSuspendBitLocker=enable" -Label "DCU"
    
                switch ($dellExit) {
                    0 {
                        Write-Status OK "All Dell updates applied successfully."
                        $State.ManufacturerPhase = 3
                        Set-ScriptState -State $State
                        return $true
                    }
                    1 {
                        Write-Status WARN "Updates applied - reboot required."
                        $State.ManufacturerPhase = 3
                        Schedule-RebootAndContinue -State $State -Reason "Dell updates (exit 1) require reboot"
                    }
                    5 {
                        Write-Status WARN "Reboot required to finish updates."
                        $State.ManufacturerPhase = 3
                        Schedule-RebootAndContinue -State $State -Reason "Dell updates (exit 5) require reboot"
                    }
                    3 {
                        Write-Status OK "System already up to date."
                        $State.ManufacturerPhase = 3
                        Set-ScriptState -State $State
                        return $true
                    }
                    default {
                        Write-Status WARN "Apply-updates returned $dellExit - continuing."
                        $State.ManufacturerPhase = 3
                        Set-ScriptState -State $State
                        return $true
                    }
                }
            }
            default {
                Write-Status OK "Dell updates already completed."
                return $true
            }
        }
        return $true
    }
    
    #===================================================================
    #  HP UPDATE LOGIC  (fully verbose - no spinners)
    #===================================================================
    function Invoke-HPUpdates {
        param([pscustomobject]$State)
        Write-Banner "HP SYSTEM UPDATE"
    
        $hpiaPaths = @(
            'C:\HP\HPIA\HPImageAssistant.exe',
            'C:\Program Files\HP\HPIA\HPImageAssistant.exe',
            'C:\Program Files (x86)\HP\HPIA\HPImageAssistant.exe'
        )
        $hpia = $hpiaPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
    
        if (-not $hpia) {
            $installed = Install-ChocoPackage -PackageName "hpimageassistant" -DisplayName "HP Image Assistant"
            if ($installed) { Start-Sleep -Seconds 5 }
            $hpia = $hpiaPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
        }
    
        if (-not $hpia) {
            Write-Status ERROR "HP Image Assistant still missing - aborting HP section."
            return $false
        }
    
        Write-Status OK "HP Image Assistant located: $hpia"
        Write-Host ""
    
        Write-SubBanner "HP Image Assistant: Analyze & Install All Updates"
        Write-Status STEP "Scanning for BIOS, firmware, driver, and software updates."
        Write-Status STEP "Each update name and install progress will be displayed below."
        Write-Host ""
    
        $hpiaArgs = "/Operation:Analyze /Action:Install /Selection:All /Silent /Noninteractive /ReportFolder:C:\Temp\HPIA"
        $hpExit = Invoke-CommandVerbose -FilePath $hpia -Arguments $hpiaArgs -Label "HPIA"
    
        switch ($hpExit) {
            0    { Write-Status OK "HP updates completed successfully."; return $true }
            256  { Write-Status WARN "HP updates require reboot (exit $hpExit)."; return $true }
            3010 { Write-Status WARN "HP updates require reboot (exit $hpExit)."; return $true }
            default {
                Write-Status WARN "HP updates returned $hpExit - continuing."
                return $true
            }
        }
    }
    
    #===================================================================
    #  MAIN EXECUTION
    #===================================================================
    try {
        if (-not (Initialize-PersistentScript)) {
            throw "Failed to initialise persistent script."
        }
        Start-Transcript -Path $Script:LogFile -Append -Force
        Write-Host ""
        Write-Host "======================================================================" -ForegroundColor Cyan
        Write-Host "       DRIVER UPDATE SCRIPT - PowerShell $($PSVersionTable.PSVersion)" -ForegroundColor Cyan
        Write-Host "======================================================================" -ForegroundColor Cyan
        Write-Host "  User:       $env:USERNAME" -ForegroundColor Cyan
        Write-Host "  Computer:   $env:COMPUTERNAME" -ForegroundColor Cyan
        Write-Host "  PID:        $PID" -ForegroundColor Cyan
        Write-Host "  Time:       $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan
        Write-Host "  Script:     $Script:PersistentScript" -ForegroundColor Cyan
        Write-Host "  Timeout:    $Script:DriverTimeout seconds per driver" -ForegroundColor Cyan
        Write-Host "  ChocoSrc:   $Script:ChocoSourceName -> $Script:ChocoSourceUrl" -ForegroundColor Cyan
        Write-Host "  Mode:       TARGETED (device-matched) + VERBOSE output" -ForegroundColor Cyan
        Write-Host "======================================================================" -ForegroundColor Cyan
        Write-Host ""
        Write-Status INFO "Checking for orphan driver processes..."
        $orphanKilled = Stop-DriverInstallProcesses -Silent
        if ($orphanKilled -gt 0) {
            Write-Status WARN "Terminated $orphanKilled leftover processes."
            Start-Sleep -Seconds 2
        }
        $state = Get-ScriptState
        Write-Host "----------------------------------------------------------------------" -ForegroundColor DarkGray
        Write-Host "  CURRENT STATE" -ForegroundColor DarkGray
        Write-Host "----------------------------------------------------------------------" -ForegroundColor DarkGray
        foreach ($prop in $state.PSObject.Properties) {
            Write-Host ("  {0,-25}: {1}" -f $prop.Name, $prop.Value) -ForegroundColor DarkGray
        }
        Write-Host "----------------------------------------------------------------------" -ForegroundColor DarkGray
        Write-Host ""
    
        # PHASE-0: Ventoy offline drivers
        if ($state.Phase -eq 0) {
            if (-not $state.VentoyCompleted) {
                $ventoyResult = Install-VentoyDrivers -State $state
                $state.TotalDriversAdded = $ventoyResult.TotalAdded
                $state.TotalDriversInstalled = $ventoyResult.TotalInstalled
                $state.TotalDriversSkipped = $ventoyResult.TotalSkippedNoMatch
                $state.TotalDriversTimedOut = $ventoyResult.TotalTimedOut
                $state.TotalProcessesKilled = $ventoyResult.TotalKilled
                $state.TotalTimeSpent = $ventoyResult.TotalTime
                if ($ventoyResult.NotFound) {
                    Write-Status INFO "No Ventoy drivers - moving to online phase."
                    $state.VentoyCompleted = $true; $state.Phase = 1
                    Set-ScriptState -State $state
                }
                elseif ($ventoyResult.NeedsReboot) {
                    Write-Status STEP "Ventoy driver install requires reboot."
                    $state.VentoyCompleted = $true; $state.VentoyRebootDone = $false
                    Set-ScriptState -State $state
                    Schedule-RebootAndContinue -State $state -Reason "Ventoy driver install - reboot required"
                }
                else {
                    Write-Status OK "Ventoy drivers installed - no reboot needed."
                    $state.VentoyCompleted = $true; $state.Phase = 1
                    Set-ScriptState -State $state
                }
            }
            else {
                Write-Status OK "Post-reboot: Ventoy driver installation finalized."
                $state.VentoyRebootDone = $true; $state.Phase = 1
                Set-ScriptState -State $state
            }
        }
    
        # PHASE-1: Online updates
        if ($state.Phase -ge 1) {
            Write-Banner "PHASE 1: ONLINE UPDATES"
            if (-not (Test-InternetConnectivity -MaxRetries 15 -RetryDelay 10)) {
                Write-Status ERROR "No internet - cannot continue."
                Write-Status INFO "Connect to a network and re-run the script."
                try { Stop-Transcript } catch { }
                exit 1
            }
            Install-ChocolateyIfNeeded
            $manufacturer = Get-SystemManufacturer
            if ($manufacturer -like '*Dell*') {
                Invoke-DellUpdates -State $state | Out-Null
            }
            elseif ($manufacturer -match 'HP|Hewlett[-\s]?Packard') {
                Invoke-HPUpdates -State $state | Out-Null
            }
            else {
                Write-Status WARN "Unsupported manufacturer: $manufacturer - only Dell and HP are handled."
            }
        }
    
        # FINAL CLEAN-UP
        Write-Status INFO "Final clean-up - terminating any remaining driver processes."
        Stop-DriverInstallProcesses -Silent | Out-Null
        Write-Host ""
        Write-Host "======================================================================" -ForegroundColor Green
        Write-Host "      DRIVER UPDATE SCRIPT COMPLETED SUCCESSFULLY!" -ForegroundColor Green
        Write-Host "======================================================================" -ForegroundColor Green
        Write-Host ""
        Write-Host "  Total reboots:          $($state.RebootCount)" -ForegroundColor White
        Write-Host "  Drivers added:          $($state.TotalDriversAdded)" -ForegroundColor White
        Write-Host "  Drivers installed:      $($state.TotalDriversInstalled)" -ForegroundColor White
        Write-Host "  Drivers skipped:        $($state.TotalDriversSkipped)" -ForegroundColor White
        Write-Host "  Drivers failed:         $($state.TotalDriversFailed)" -ForegroundColor White
        Write-Host "  Drivers timed out:      $($state.TotalDriversTimedOut)" -ForegroundColor White
        Write-Host "  Processes killed:       $($state.TotalProcessesKilled)" -ForegroundColor White
        Write-Host "  Total time spent:       $(Format-Duration $state.TotalTimeSpent)" -ForegroundColor White
        Write-Host "  Dell ADR failed:        $($state.DellADRFailed)" -ForegroundColor White
        Write-Host "  Choco source:           $($Script:ChocoSourceName)" -ForegroundColor White
        Write-Host ""
        Write-Host "======================================================================" -ForegroundColor Green
        Write-Host ""
        Remove-ScriptState
    }
    catch {
        Write-Host ""
        Write-Host "======================================================================" -ForegroundColor Red
        Write-Host "  SCRIPT ERROR" -ForegroundColor Red
        Write-Host "======================================================================" -ForegroundColor Red
        Write-Status ERROR "Message: $($_.Exception.Message)"
        Write-Status ERROR "Line:    $($_.InvocationInfo.ScriptLineNumber)"
        Write-Host $_.ScriptStackTrace -ForegroundColor Red
        Write-Host ""
        Write-Status INFO "Attempting emergency clean-up..."
        try {
            $saveEA = $ErrorActionPreference
            $ErrorActionPreference = 'SilentlyContinue'
            Stop-DriverInstallProcesses -Silent | Out-Null
            $ErrorActionPreference = $saveEA
        }
        catch { try { $ErrorActionPreference = $saveEA } catch { } }
        Write-Status INFO "State file kept - re-run script to continue."
    }
    finally {
        try { Stop-Transcript } catch { }
    }
    
    
  • Windows 11 25H2 File Explorer Rename Bug

    Space key opens the file instead of inserting a space

    Environment

    • OS: Windows 11 25H2
    • Build: 26200.7462
    • Component: File Explorer (explorer.exe)

    🐞 Problem description -

    When renaming a file or folder in Windows 11 File Explorer, pressing the Space key may unexpectedly open the file instead of inserting a space in the filename.

    This issue typically occurs when:

    • Renaming via mouse click (not F2)
    • Typing immediately after clicking the filename
    • Explorer loses keyboard focus during rename

    The behavior is inconsistent and highly disruptive for users who rename files frequently.


    🔍 Root cause analysis

    This is not a keyboard or permission issue.
    It is caused by File Explorer UI focus instability, introduced or exacerbated in recent Windows 11 builds (including 25H2).

    Two internal Explorer behaviors contribute to the bug:

    1️⃣ Modern context menu / shell UI interaction

    The Windows 11 modern shell UI aggressively intercepts input events.
    If Explorer momentarily loses rename focus, the Space key is interpreted as a command, not text input — resulting in Open file.

    2️⃣ Folder type auto-discovery (content sniffing)

    Explorer continuously analyzes folder contents to decide whether it’s:

    • Documents
    • Pictures
    • Music
    • Videos

    This background reclassification can trigger UI refreshes, which:

    • Cancel rename edit mode
    • Move keyboard focus back to the file item
    • Cause the next key press (e.g. Space) to act on the file instead of the text box

    ✅ Solution: Registry-based workaround

    While Microsoft has not yet released an official fix, disabling the problematic behaviors stabilizes File Explorer and prevents focus loss during rename.

    What the fix does

    • Restores the classic context menu behavior
    • Disables folder type auto-discovery
    • Reduces Explorer UI refresh events
    • Keeps rename text box focused correctly

    🛠️ Registry fix (.reg file)

    Save the following as Explorer_Rename_Stability_Win11_25H2.reg
    and import it (double-click → Yes).

    Windows Registry Editor Version 5.00
    
    ; ==========================================
    ; Windows 11 Explorer Rename Stability Tweaks
    ; Target: Win11 25H2 (26200.x)
    ; ==========================================
    
    ; --- Tweak 1: Restore classic context menu
    ; Reduces Explorer UI focus glitches
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell\Update\Packages]
    "UndockingDisabled"=dword:00000001
    
    
    ; --- Tweak 2: Disable folder type auto-discovery
    ; Prevents Explorer rescans that break rename focus
    [HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags\AllFolders\Shell]
    "FolderType"="NotSpecified"
    

    🔄 Apply the changes

    After importing the registry file:

    • Restart Windows Explorer, or
    • Reboot the system (recommended)

    🧪 Result

    After applying this fix:

    • Pressing Space correctly inserts a space while renaming
    • Files no longer open unexpectedly
    • Rename behavior is stable and predictable

    This workaround has been confirmed effective on:

    • Windows 11 25H2 – build 26200.7462

    ⚠️ Notes

    • This is a workaround, not an official Microsoft fix
    • The underlying issue is a File Explorer UI bug
    • Using F2 to rename remains the most reliable native method
    • The registry change is safe and reversible

    🧹 Reverting the change

    To undo the fix, remove the added registry values or use a revert .reg file.

    Save this as Explorer_Rename_Stability_Revert.reg

    Windows Registry Editor Version 5.00
    
    ; --- Revert classic context menu tweak
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell\Update\Packages]
    "UndockingDisabled"=-
    
    ; --- Revert folder type tweak
    [HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags\AllFolders\Shell]
    "FolderType"=-
    

    📌 Conclusion

    The “Space opens file while renaming” issue in Windows 11 25H2 is caused by Explorer UI focus loss, not user error.

    By simplifying Explorer’s UI behavior through targeted registry tweaks, the issue can be effectively eliminated.

    Until Microsoft addresses this at the Explorer level, this solution provides a stable and practical fix.

  • Free file sync compare a synology folder to teams folder with exclusion filters

    COMPARE-ONLY, safe for Synology ↔ Teams.

    ✅ Compare-Only .ffs_gui (Synology ↔ Teams)

    👉 Replace your file entirely with the content below.

    Save as:

    Synology_vs_Teams_COMPARE_ONLY.ffs_gui
    

    📄 FreeFileSync config (NO warnings)

    <?xml version="1.0" encoding="utf-8"?>
    <FreeFileSync XmlType="GUI" XmlFormat="18">
    
        <Notes>Compare-only configuration for Synology NAS ↔ Microsoft Teams (OneDrive local sync)</Notes>
    
        <Compare>
            <Variant>TimeAndSize</Variant>
            <Symlinks>Exclude</Symlinks>
            <IgnoreTimeShift/>
        </Compare>
    
        <!-- Synchronization deliberately disabled -->
        <Synchronize>
            <Variant>Custom</Variant>
    
            <CustomDirections>
                <LeftOnly>None</LeftOnly>
                <RightOnly>None</RightOnly>
                <LeftNewer>None</LeftNewer>
                <RightNewer>None</RightNewer>
            </CustomDirections>
    
            <DetectMovedFiles>false</DetectMovedFiles>
            <DeletionPolicy>Permanent</DeletionPolicy>
    
            <VersioningFolder Style="None"/>
        </Synchronize>
    
        <Filter>
            <Include>
                <Item>*</Item>
            </Include>
    
            <Exclude>
                <!-- Office / Teams temporary files -->
                <Item>~$*</Item>
                <Item>*.tmp</Item>
                <Item>*.temp</Item>
                <Item>*.bak</Item>
                <Item>*.wbk</Item>
                <Item>*.swp</Item>
                <Item>*.lock</Item>
                <Item>*~</Item>
                <Item>~*.tmp</Item>
    
                <!-- Synology / NAS metadata -->
                <Item>@eaDir/*</Item>
                <Item>#recycle/*</Item>
                <Item>.recycle/*</Item>
                <Item>.nfs*</Item>
                <Item>.fuse_hidden*</Item>
    
                <!-- Windows / OneDrive artifacts -->
                <Item>Thumbs.db</Item>
                <Item>desktop.ini</Item>
                <Item>*.lnk</Item>
                <Item>$RECYCLE.BIN/*</Item>
                <Item>System Volume Information/*</Item>
    
                <!-- macOS clients -->
                <Item>.DS_Store</Item>
    
                <!-- Cache -->
                <Item>.cache/*</Item>
            </Exclude>
    
            <SizeMin>0</SizeMin>
            <SizeMax>0</SizeMax>
            <TimeSpan/>
        </Filter>
    
        <FolderPairs>
            <Pair>
                <!-- Synology NAS (SMB/CIFS) -->
                <Left>\\NAS\SHARE\FOLDER</Left>
    
                <!-- Microsoft Teams (OneDrive local folder) -->
                <Right>C:\Users\USERNAME\OneDrive - ORGANIZATION\Teams\TEAM_NAME</Right>
            </Pair>
        </FolderPairs>
    
        <Errors IgnoreErrors="false"/>
    
        <PostSyncCommand Condition="None"/>
    
        <MainConfig>
            <FailSafe>true</FailSafe>
            <AutoRetry count="2" delay="5"/>
            <ProgressDialog>true</ProgressDialog>
        </MainConfig>
    
        <LogFolder/>
    
        <EmailNotification/>
    
        <Gui>
            <GridViewType>Difference</GridViewType>
        </Gui>
    
    </FreeFileSync>
    

    🔧 What you MUST edit

    Only change these paths:

    Synology

    \\NAS\SHARE\FOLDER
    

    Example:

    \\192.168.1.20\Projects
    

    Teams (local OneDrive path)

    C:\Users\USERNAME\OneDrive - ORGANIZATION\Teams\TEAM_NAME
    

    🧪 Expected behavior now

    ✔ No XML warnings ✔ Compare button works immediately ✔ Synchronize does nothing (all directions = None) ✔ Safe for audits / verification ✔ Stable with CIFS + OneDrive


  • Provisioning New HP Laptop after clonage

    Provisioning New HP Laptop after clonage

    • This Script renames the Pc Name
    • Feeds the Asset Tag to Bios
    • Updates the BIOS
    #Requires -RunAsAdministrator
    <#
    .SYNOPSIS
        HP BIOS Update, PC Rename, and Asset Tag Configuration Script
    .DESCRIPTION
        Prompts for PC name and Asset Tag, configures settings, and launches BIOS update silently
    .NOTES
        Requires HP Client Management Script Library (HPCMSL)
        Run as Administrator
        Place the 'bin' folder from extracted Softpaq in the same location as this script
    #>
    
    # ========================== CONFIGURATION ==========================
    $ScriptDirectory = $PSScriptRoot
    $BiosExe = Join-Path -Path $ScriptDirectory -ChildPath "bin\HpFirmwareUpdRec.exe"
    
    # ========================== MAIN SCRIPT ==========================
    
    Clear-Host
    Write-Host "==========================================" -ForegroundColor Cyan
    Write-Host "   HP BIOS & System Configuration Tool   " -ForegroundColor Cyan
    Write-Host "==========================================" -ForegroundColor Cyan
    Write-Host ""
    
    # Check if BIOS file exists
    if (-not (Test-Path -Path $BiosExe)) {
        Write-Host "ERROR: HpFirmwareUpdRec.exe not found!" -ForegroundColor Red
        Write-Host "Expected path: $BiosExe" -ForegroundColor Yellow
        Write-Host ""
        Write-Host "Make sure the 'bin' folder is in: $ScriptDirectory" -ForegroundColor Yellow
        Read-Host "Press Enter to exit"
        exit 1
    }
    
    Write-Host "BIOS Update file found: $BiosExe" -ForegroundColor Green
    Write-Host ""
    
    # Check if HP CMSL module is available
    if (-not (Get-Module -ListAvailable -Name "HPCMSL")) {
        Write-Host "WARNING: HP CMSL module not found." -ForegroundColor Yellow
        $InstallModule = Read-Host "Do you want to install it now? (Y/N)"
        
        if ($InstallModule -match '^[Yy]$') {
            Write-Host "Installing HP CMSL module..." -ForegroundColor Yellow
            Install-Module -Name HPCMSL -Force -AcceptLicense
            Import-Module HPCMSL
            Write-Host "HP CMSL module installed successfully." -ForegroundColor Green
        }
        else {
            Write-Host "Cannot proceed without HP CMSL module." -ForegroundColor Red
            Read-Host "Press Enter to exit"
            exit 1
        }
    }
    
    # Display current info
    Write-Host "Current PC Name: $env:COMPUTERNAME" -ForegroundColor Gray
    Write-Host ""
    
    # ========================== USER INPUT ==========================
    
    # Get new PC Name
    do {
        $NewPCName = Read-Host "Enter the NEW PC Name"
        if ([string]::IsNullOrWhiteSpace($NewPCName)) {
            Write-Host "PC Name cannot be empty!" -ForegroundColor Red
        }
        elseif ($NewPCName.Length -gt 15) {
            Write-Host "PC Name cannot exceed 15 characters!" -ForegroundColor Red
            $NewPCName = $null
        }
    } while ([string]::IsNullOrWhiteSpace($NewPCName))
    
    # Get Asset Tracking Number
    do {
        $AssetTag = Read-Host "Enter the Asset Tracking Number"
        if ([string]::IsNullOrWhiteSpace($AssetTag)) {
            Write-Host "Asset Tracking Number cannot be empty!" -ForegroundColor Red
        }
    } while ([string]::IsNullOrWhiteSpace($AssetTag))
    
    # ========================== CONFIRMATION ==========================
    
    Write-Host ""
    Write-Host "==========================================" -ForegroundColor Yellow
    Write-Host "  New PC Name        : $NewPCName" -ForegroundColor White
    Write-Host "  Asset Tracking No. : $AssetTag" -ForegroundColor White
    Write-Host "==========================================" -ForegroundColor Yellow
    Write-Host ""
    
    $Confirm = Read-Host "Proceed with these settings? (Y/N)"
    if ($Confirm -notmatch '^[Yy]$') {
        Write-Host "Operation cancelled." -ForegroundColor Yellow
        exit 0
    }
    
    Write-Host ""
    
    # ========================== SET ASSET TRACKING NUMBER ==========================
    
    Write-Host "[1/3] Setting Asset Tracking Number..." -ForegroundColor Yellow
    
    try {
        Set-HPBIOSSettingValue -Name "Asset Tracking Number" -Value $AssetTag
        Write-Host "      SUCCESS: Asset Tracking Number set to '$AssetTag'" -ForegroundColor Green
    }
    catch {
        Write-Host "      ERROR: Failed to set Asset Tracking Number" -ForegroundColor Red
        Write-Host "      $($_.Exception.Message)" -ForegroundColor Red
    }
    
    # ========================== RENAME COMPUTER ==========================
    
    Write-Host "[2/3] Renaming computer to '$NewPCName'..." -ForegroundColor Yellow
    
    try {
        Rename-Computer -NewName $NewPCName -Force -ErrorAction Stop
        Write-Host "      SUCCESS: Computer will be renamed after restart" -ForegroundColor Green
    }
    catch {
        Write-Host "      ERROR: Failed to rename computer" -ForegroundColor Red
        Write-Host "      $($_.Exception.Message)" -ForegroundColor Red
    }
    
    # ========================== LAUNCH BIOS UPDATE (SILENT) ==========================
    
    Write-Host "[3/3] Running BIOS Update (Silent Mode)..." -ForegroundColor Yellow
    Write-Host "      Please wait, this may take a few minutes..." -ForegroundColor Gray
    Write-Host "      DO NOT turn off the computer!" -ForegroundColor Red
    
    try {
        # Silent install: -r (reboot) -b (bitlocker suspend) -s (silent)
        $Process = Start-Process -FilePath $BiosExe -ArgumentList "-r -b -s" -Wait -PassThru
        
        switch ($Process.ExitCode) {
            0 { Write-Host "      SUCCESS: BIOS Update completed" -ForegroundColor Green }
            256 { Write-Host "      INFO: BIOS is already up to date" -ForegroundColor Cyan }
            273 { Write-Host "      SUCCESS: BIOS Update staged for reboot" -ForegroundColor Green }
            282 { Write-Host "      INFO: BIOS downgrade blocked by policy" -ForegroundColor Yellow }
            3010 { Write-Host "      SUCCESS: BIOS Update completed (restart required)" -ForegroundColor Green }
            default { Write-Host "      Exit code: $($Process.ExitCode)" -ForegroundColor Yellow }
        }
    }
    catch {
        Write-Host "      ERROR: Failed to run BIOS update" -ForegroundColor Red
        Write-Host "      $($_.Exception.Message)" -ForegroundColor Red
    }
    
    # ========================== COMPLETION ==========================
    
    Write-Host ""
    Write-Host "==========================================" -ForegroundColor Cyan
    Write-Host "            SCRIPT COMPLETED             " -ForegroundColor Cyan
    Write-Host "==========================================" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "RESTART REQUIRED to apply all changes!" -ForegroundColor Yellow
    Write-Host ""
    
    $Restart = Read-Host "Restart now? (Y/N)"
    if ($Restart -match '^[Yy]$') {
        Restart-Computer -Force
    }
    

    Folder Structure

    Your folder should look like this: Extract the contents bios.exe (in our case ex: SP169859.exe) and rename it as bin and place it by the side of the powershell script.

    📁 YourFolder
    ├── 📄 Setup.ps1 (this script)
    └── 📁 bin
        ├── 📄 HpFirmwareUpdRec.exe
        └── 📄 (other files from Softpaq)
    

    Silent Install Switches Explained

    Switch Description
    -s Silent mode (no user prompts)
    -b Suspend BitLocker automatically
    -r Reboot automatically after update

    Other Useful Switches

    Switch Description
    -f Force update even if same version
    -p Password (if BIOS is password protected)

    If BIOS Has Password

    $Process = Start-Process -FilePath $BiosExe -ArgumentList "-r -b -s -p YourPassword" -Wait -PassThru
    
  • Add GSD files to Tia-Portal

    Copying the GSD files to the corresponding repository will not work :

    This is key: TIA Portal V19 does not automatically scan every folder you drop XMLs into, and simply copying files into xdd\gsd doesn’t make them appear in the Hardware Catalog. There’s an internal registration/indexing step that the GUI import normally does.

    So, to make the Dataclogic M120 show up without using the GUI, we need to emulate what the GUI does behind the scenes. Here’s the detailed breakdown.


    1. How TIA Portal actually “registers” a GSD

    When you import via:

    Options → Manage GSD files → Install
    

    TIA does three things:

    1. Copies files to the system GSD folder Usually:

      C:\ProgramData\Siemens\Automation\Portal V19\data\xdd\gsd
      
    2. Creates/updates a device registry cache (binary index) under:

      C:\Users\<User>\AppData\Roaming\Siemens\Automation\Portal V19\Data\xdd
      

      or sometimes:

      C:\ProgramData\Siemens\Automation\Portal V19\Data\xdd\cache
      
    3. Validates the GSD XML against the supported version schema (V2.3/V2.4)

    ✅ The crucial part: without updating the cache, TIA Portal won’t display the device, even if the XML is physically in the folder.


    2. Why your manual copy isn’t working

    • You copied the .xml files into xdd\gsd manually.
    • TIA did not rebuild the cache/index, so it doesn’t “see” the new device.
    • TIA Portal only reads new GSDs during GUI import or after the internal indexing process.

    3. How to make it work without GUI

    There are two practical approaches:

    TIA Portal supports a command-line import of GSDs via PortalCmd.exe (hidden Siemens tool):

    "C:\Program Files\Siemens\Automation\Portal V19\PortalCmd.exe" /GSDInstall:"C:\Path\To\GSDML"
    
    • This will copy the GSD and update the internal index.
    • Fully silent, scriptable, ideal for multiple lab PCs.

    ⚠️ Note: PortalCmd.exe exists in V17+; V19 retains it. It’s undocumented but works reliably.


    Option B: Pre-build the xdd folder and cache

    If you don’t want to rely on PortalCmd:

    1. Do a manual import once on a master PC

      • Dataclogic M120 appears in Hardware Catalog.
    2. Copy All folders from C:\ProgramData\Siemens\Automation\Portal V19\data it includes folder like hwcn,xdd,xddint to all lab PCs:

    C:\ProgramData\Siemens\Automation\Portal V19\data\xdd\gsd
    C:\Users\<User>\AppData\Roaming\Siemens\Automation\Portal V19\Data\xdd
    
    • This ensures the XML and the user/device index cache are present.
    • When students open TIA, the device appears instantly.

    Essentially, you are cloning the “registered” GSD environment.


    1. Install M120 GSD on a single PC via GUI.
    2. Locate these folders:
    C:\ProgramData\Siemens\Automation\Portal V19\data\*  <-- XML + icons
    # The below path is not required as i hev tested it works well with the folder programdata
    C:\Users\<AdminUser>\AppData\Roaming\Siemens\Automation\Portal V19\Data\*  <-- cache/index
    
    1. Copy hwcn,xdd,xddint to all lab PCs (maintain structure).
    2. Make sure TIA is closed during copy.
    3. Launch TIA — device appears without GUI import.

    This is the most reliable method for lab-wide automation.


  • ChatGPT Enhanced Markdown Copy with Code Blocks ,Tables and Formula

    ChatGPT Enhanced Markdown Copy with Code Blocks ,Tables and Formula: Download link

    ChatGPT/Claude/Grok/Arena | Conversation/Chat Markdown Export/Download : Download link

    Tampermonkey Script Security Audit Report

    Executive Summary

    This script is relatively safe to use for its intended purpose. However, there are several medium-risk issues and best practices violations that warrant attention. The script does not attempt to exfiltrate data or access sensitive information, but it has code quality and security concerns that should be addressed.

    Risk Level: MEDIUM ⚠️


    Detailed Security Analysis

    1. External Dependency Risk (MEDIUM RISK)

    Issue: The script loads Turndown.js from a CDN:

    // @require https://cdnjs.cloudflare.com/ajax/libs/turndown/7.1.2/turndown.min.js
    

    Vulnerability Details:

    • The script relies on cdnjs.cloudflare.com to provide the Turndown library

    • If this CDN is compromised, malicious code could be injected

    • No integrity checking (Subresource Integrity / SRI hash) is used

    • The minified nature of the library means you cannot easily audit it

    Risk Assessment:

    • Cloudflare’s CDN is generally trustworthy, but this is still a theoretical attack vector

    • The library is widely used and monitored by security researchers

    • Likelihood of compromise: Low, but possible

    Recommendation:
    Add a Subresource Integrity (SRI) hash to verify the library hasn’t been tampered with:

    // @require https://cdnjs.cloudflare.com/ajax/libs/turndown/7.1.2/turndown.min.js#sha384-[hash-here]
    

    Alternatively, embed the Turndown library directly in the script (it’s reasonably compact in minified form) to eliminate the CDN dependency.


    2. No Permissions Declaration (LOW RISK)

    Issue: The script declares @grant none, which is correct:

    // @grant        none
    

    Why This Matters:

    • Positive finding: The script doesn’t request clipboard manipulation, storage access, or network capabilities

    • The script properly uses standard browser APIs (navigator.clipboard) instead

    • This significantly limits what the script can do maliciously

    Assessment: This is actually a security best practice being followed correctly.


    3. XSS Vulnerability in Table Processing (HIGH RISK)

    Critical Issue Found:

    tempDiv.innerHTML = cell.innerHTML;
    // ... then processed with turndownService.turndown()
    

    Vulnerability Details:

    • When processing table cells, the script uses innerHTML directly

    • If ChatGPT’s HTML response contains malicious content, it could potentially be executed

    • However, the Turndown library’s turndown() method converts HTML to Markdown text, which mitigates this significantly

    • Actual risk is mitigated because the output is text-based, not re-injected into the DOM

    Severity: Medium (mitigated by Turndown conversion)

    Recommendation:
    Use textContent for extracting cell data when safety is the priority:

    const tempDiv = document.createElement('div');
    tempDiv.textContent = cell.textContent;
    

    However, this might lose some formatting. A safer middle ground:

    const tempDiv = document.createElement('div');
    // Clone and sanitize
    const clone = cell.cloneNode(true);
    tempDiv.appendChild(clone);
    

    4. DOM Cloning Security (LOW RISK)

    Code:

    const range = selection.getRangeAt(0);
    tempDiv.appendChild(range.cloneContents());
    

    Assessment:

    • ✅ The script clones selected content from the user’s selection, not arbitrary DOM

    • Only processes what the user explicitly selects

    • This is safe because users are choosing what to copy

    • No risk of unauthorized content access


    5. Clipboard Access (MEDIUM RISK)

    Code:

    navigator.clipboard.writeText(markdownText)
        .then(() => { ... })
        .catch((err) => { ... });
    

    Security Assessment:

    • ✅ Uses modern navigator.clipboard API (not legacy document.execCommand())

    • ✅ Only writes to clipboard when user explicitly triggers the shortcut

    • ✅ Does not read from clipboard (safer than two-way access)

    • The script correctly asks for no clipboard permissions since it uses standard API

    • No exfiltration risk - clipboard is a user-controlled resource

    Assessment: Safe implementation.


    6. Scope and Match Directives (LOW RISK)

    Code:

    // @match       *://chatgpt.com/c
    // @match       *://chatgpt.com/c/*
    

    Assessment:

    • Excellent scope limitation: Script only runs on ChatGPT conversation pages

    • ✅ Prevents the script from running on unintended websites

    • ✅ Properly uses wildcard protocol matching (*://)

    • No risk of unintended site access

    Assessment: Security best practice followed.


    7. Code Quality Issues (LOW RISK)

    Issue 1: Inconsistent Error Handling

    console.warn("No content selected for copying.");
    // No graceful fallback, just returns silently
    

    Issue 2: Regex Pattern

    .replace(/\n{2,}/g, '\n\n')
    
    • Safe regex, no ReDoS (Regular Expression Denial of Service) vulnerability

    • However, this could collapse more than 2 newlines, which might be intentional but isn’t documented

    Issue 3: Commented-out Code

    //alert("Enhanced Markdown copied!v5");
    
    • Should be removed in production

    • Leaving it doesn’t create a security risk, but reduces code cleanliness

    Assessment: Code quality issues, but not security vulnerabilities.


    8. No Data Exfiltration (✅ SAFE)

    Thorough Check:

    • ❌ No fetch() calls to external servers

    • ❌ No XMLHttpRequest to unknown domains

    • ❌ No GM_xmlhttpRequest() (Tampermonkey API)

    • ❌ No WebSocket connections

    • ❌ No image beacons or tracking pixels

    • ❌ No localStorage or sessionStorage access

    • ❌ No GM_setValue() or persistent storage

    Assessment: No data exfiltration mechanisms present. The script only writes to clipboard locally.


    9. Input Validation (MEDIUM RISK)

    Weak Point:
    The script doesn’t validate that the selected content is actually from ChatGPT:

    const selection = window.getSelection();
    if (!selection || selection.isCollapsed) {
        console.warn("No content selected for copying.");
        return;
    }
    // No origin verification - could be any selected HTML
    

    Risk: Low in practice because:

    • Users are deliberately selecting content

    • The conversion is transparent (generates readable Markdown)

    • Output is saved locally to clipboard

    Recommendation: Add a check that selection comes from ChatGPT’s container:

    const chatContainer = document.querySelector('[data-testid="conversation"]');
    if (!chatContainer || !chatContainer.contains(selection.anchorNode)) {
        console.warn("Selection must be from ChatGPT conversation");
        return;
    }
    

    Security Findings Summary

    Category Finding Severity Status
    External Dependencies CDN-loaded library without SRI hash Medium ⚠️ Should be fixed
    Data Exfiltration No unauthorized data transmission None ✅ Safe
    XSS Vectors HTML processing via innerHTML Medium ✅ Mitigated by Turndown
    Clipboard Abuse Proper clipboard access only None ✅ Safe
    Scope Control Limited to ChatGPT domains None ✅ Safe
    Code Injection No eval() or Function() constructors None ✅ Safe
    Permissions Minimal, correct declarations None ✅ Safe

    Recommendations for Improved Security

    Priority 1 (Should Do)

    1. Add SRI hash to Turndown library or embed it directly

    2. Remove commented-out alert line (line 89)

    3. Add input validation to ensure selection is from ChatGPT content area

    Priority 2 (Nice to Have)

    1. Use textContent instead of innerHTML in table cell processing

    2. Add error notifications to inform user if copy fails (currently silent)

    3. Add version fingerprinting or integrity checks to the metadata

    Priority 3 (Code Quality)

    1. Add JSDoc comments for the enhancedCopy() function

    2. Handle edge cases where no content is available more gracefully

    3. Test with different table formats from ChatGPT to ensure compatibility


    Final Safety Verdict

    Is this script safe to use? YES, with caveats.

    Risk Assessment:

    • Personal data safety: ✅ No risk (no exfiltration, no credential theft)

    • System compromise: ✅ No risk (no execution exploits, no malware payloads)

    • Account security: ✅ No risk (doesn’t access ChatGPT tokens or session data)

    • Privacy: ✅ Safe (only processes clipboard, which you control)

    When to Use This Script:

    • ✅ Safe to use for converting ChatGPT responses to Markdown

    • ✅ Safe to use on your personal account (not sensitive environments)

    • ✅ Safe to use for general productivity

    When to Avoid:

    • ❌ Don’t use if you handle sensitive information (though the script doesn’t steal it, principle of least risk)

    • ❌ Don’t use without verifying the CDN dependency occasionally

    • ❌ Don’t use if you’re uncomfortable with Cloudflare serving code to your browser

    Action Items:

    1. Install and test on a low-risk conversation first

    2. Monitor the Tampermonkey dashboard to ensure only expected network calls occur

    3. Watch for future updates from the author (check GreasyFork periodically)

    4. Consider requesting the author add SRI hash verification


    How to Monitor After Installation

    After installing, verify the script is behaving as expected:

    1. Open Browser DevTools (F12 → Network tab)

    2. Select some ChatGPT text and press Alt + Shift + C

    3. Check Network tab — you should only see:

      • Initial load of turndown.min.js from cdnjs.cloudflare.com

      • No other external requests

    4. Check Console — should see: "Shortcut Alt + Shift + C is ready for enhanced copying."

    5. Paste the clipboard content somewhere to verify it’s valid Markdown

    If you see any unexpected network requests or console errors, disable the script immediately.

  • Enable DHCP on S7-1500

    To set a Siemens S7-1500 CPU to obtain its IP address from DHCP, follow these steps in TIA Portal:

    Enable DHCP on S7-1500

    1. Open your project in TIA Portal

    • Go to Devices & networks (or double-click the CPU).

    2. Select the CPU

    • In the project tree, click on CPU → Properties.

    3. Go to PROFINET interface settings

    • Under Properties, open:
      → PROFINET interface [X1]
      → Ethernet addresses

    4. Change IP assignment mode

    • Find IP protocol → Assignment method.

    • Select:

    ▶ “Obtain IP address automatically (DHCP)”

    Other options in that menu:

    • Use static IP address

    • Set IP via BOOTP

    • Set IP via DCP

    • Obtain IP via DHCP ← choose this

    5. (Optional) Specify hostname for DHCP

    Some DHCP servers assign IP based on hostname.
    Under the same settings you can enter:

    • Device name (PROFINET name)

    • Hostname

    6. Download hardware configuration to the PLC

    • Click Download to device.

    • After reboot, the CPU will request its IP from the DHCP server.


    📌 Confirm DHCP operation

    You can verify:

    • On the diagnostic buffer of the CPU

    • On your DHCP server (it should list the PLC lease)

    • In TIA Portal online view → Interface details


    📝 Notes

    • The CPU must be on a network with a DHCP server (router or Windows DHCP).

    • Avoid mixing PROFINET device name settings with static IP unless needed.

    • Safety CPUs behave the same but require a password to download.


    To set DHCP directly on the S7-1500 CPU itself using the front display (the built-in HMI on the CPU), without TIA Portal.

    Here is how:


    Enable DHCP directly on the S7-1500 front display

    1. Press the “OK” button on the CPU to open the local menu.

    2. Navigate to:

    Settings → Interfaces → PROFINET / Ethernet → IP configuration

    Depending on firmware, the menu names may be slightly different:

    • IP configuration

    • Address assignment

    • IP settings

    3. Change “IP assignment”

    You will see options such as:

    • Static

    • BOOTP

    • DCP

    • DHCP

    👉 Select DHCP.

    4. Save & Reboot

    The CPU usually asks to confirm.
    After confirmation, it will restart the interface and request an IP from your DHCP server.


    ✔ Notes

    • The CPU’s display allows changing IP, subnet, gateway, and assignment method.

    • If the PLC is password-protected for access level, you may need to enter the authorization level.

    • After switching to DHCP, the CPU will lose its old static IP, so make sure you know how to find it in your DHCP server or network scanner.


    “IP settings are write-protected”, it means the CPU has a protection level or a configuration setting that prevents changing network parameters from the front display.

    Here are the common causes and how to fix them:


    ✅ 1. Protection level blocks changes

    If the CPU is set to “Full access (password protected)” or “Read access only”, you cannot change IP settings on the display.

    Fix

    Connect with TIA Portal → go to:

    Device → Protection & Security → Access level

    Set one of these:

    • Full access (no protection)
      or

    • Full access (password protected) → enter password when asked on device

    Then download the hardware configuration again.


    ✅ 2. “Write protection” enabled in TIA Portal

    In TIA Portal, under the PLC properties there is a setting that disables local changes.

    Check:

    Properties → General → Operating mode → Prevent local changes
    (or similar depending on version)

    If “IP address write-protected” or “Allow local configuration: No” is active → you must disable it.


    ✅ 3. CPU memory card (SIMATIC Memory Card) contains locked settings

    Some projects enforce IP settings through the memory card.
    In that case the CPU restores the IP from the card and blocks display changes.

    Fix

    Either:

    • Remove the memory card → reboot → change IP → insert again
      or

    • Change IP settings in TIA Portal → download to the CPU → card gets updated


    ✅ 4. The interface is part of a PROFINET system where the IP is assigned by the device name

    If the PLC is inside a PROFINET IO system, its IP may be controlled by DCP/PNIO, not manually.

    Symptoms:

    • You cannot change the IP because it is bound to the PROFINET device name.

    Fix

    In TIA Portal:

    • Go to Devices & networks → CPU → PROFINET interface

    • Look for “IP assigned by IO controller” or “Use device name assignment”

    • Disable it if you need manual IP control


    👉 What you can try immediately on the device

    1. Go to Display → Settings → Access level

    2. If it asks for a password, enter the CPU protection password

    3. Try again to change the IP settings

    If you don’t know the password, only TIA Portal download can change it.


  • Disable NVIDIA Quadro 2200 GPU from Windows 11 Command Line

    Disable NVIDIA Quadro 2200 GPU from Windows 11 Command Line

    ✅ Step 1 — Install DevCon

    DevCon is included with the Windows Driver Kit (WDK).

    After installation, DevCon is typically located at:

    C:\Program Files (x86)\Windows Kits\10\Tools\x64\devcon.exe
    

    ✅ Step 2 — Use the Device Instance ID

    Your GPU hardware ID:

    PCI\VEN_10DE&DEV_1C31&SUBSYS_131B1028&REV_A1
    

    ✅ Step 3 — Open Command Prompt as Administrator

    Search for cmd, right‑click, choose Run as administrator.

    ✅ Step 4 — Disable the GPU

    Run:

    devcon disable "PCI\VEN_10DE&DEV_1C31&SUBSYS_131B1028&REV_A1"
    

    🔄 Re-enable Later

    devcon enable "PCI\VEN_10DE&DEV_1C31&SUBSYS_131B1028&REV_A1"
    

    ℹ️ Notes

    ✔ If the exact ID doesn’t match, try using a wildcard:

    devcon disable "PCI\VEN_10DE&DEV_1C31*"
    

    ✔ List matching NVIDIA devices:

    devcon find *VEN_10DE*
    

    ✔ Wildcard disable:

    devcon disable "PCI\VEN_10DE&DEV_1C31*"
    

    ✔ To get the full instance ID from CMD:

    devmgmt.msc
    → View → Devices by connection → Properties → Details → “Device instance path”.
    
  • AI Prompts

    Useful AI Prompts


  • Désactiver Privacy Badger pour un site spécifique

    Désactiver Privacy Badger pour un site spécifique

    Ce guide explique comment désactiver temporairement Privacy Badger pour un seul site afin qu’il fonctionne correctement, tout en conservant la protection activée partout ailleurs.


    1. Ouvrir le site

    Accédez à la page Web que vous souhaitez autoriser (le site qui ne fonctionne pas lorsque Privacy Badger est actif).


    2. Ouvrir la fenêtre de Privacy Badger

    Cliquez sur l’icône de Privacy Badger dans la barre d’outils de Chrome (en haut à droite) pour ouvrir son panneau.

    image


    3. Désactiver Privacy Badger pour ce site

    Dans la fenêtre, cliquez sur « Disable for this site » (désactiver pour ce site).
    Cette action ajoute le site à la liste blanche. Rechargez ensuite la page.

    image

    Astuce : Le texte peut varier selon les versions, mais l’action reste la même — désactiver Privacy Badger pour le domaine actuel.


    4. Recharger & vérifier

    • Après avoir autorisé le site, rechargez la page.
    • Si le problème persiste, essayez un rechargement forcé (Ctrl + Maj + R) ou F5 ou ouvrez la page en navigation privée.
    • Vérifiez que le site fonctionne désormais correctement avec Privacy Badger désactivé pour ce domaine.
  • markdown-cheatsheet

    📘 Markdown Shortcuts Cheat-Sheet 📝 Basic Formatting Bold → text or text

  • AI Shortcut Prompt Codes

    Favorite Shortcut Commands

  • Enable Run as Administartor option to a shortcut in desktop

    Here is the powershell script to modify the Shortcut parameters: ```powershell param( [Parameter(Mandatory=$true)] [string]$computerListPath,

    [Parameter(Mandatory=$true)]
    [string]$shortcutName,
    
    [Parameter(Mandatory=$false)]
    [string]$logPath )
    
  • Disable a service and set the recovery to take no action.

    Here’s the Powershell script: ```powershell <# .SYNOPSIS Disables a Windows service and resets its recovery options across multiple computers.

  • Turn off Bitlocker and check the status of the decrytion.

    Here is the powershell script that turns off Bitlocker and checks the status of the decryptions.

  • Create a Custom Recovery Partition on Windows 11

    📦 PowerShell Script: Create a Custom Recovery Partition on Windows 11

    This script automates the process of capturing a custom system image and creating a dedicated recovery partition. The recovery partition can then be used with “Reset this PC” > “Keep my files” to restore the system with pre-installed software.


    ✅ Features

    • Captures current system (C:) into a .wim image using DISM
    • Creates a new recovery partition (30% of disk size)
    • Copies the .wim into Recovery\WindowsRE
    • Registers the custom image with reagentc
    • Hides the recovery partition from normal users

    ⚠️ Requirements

    • Must run as Administrator
    • DISM must be available (built into Windows 10/11)
    • Sufficient disk space to shrink C: and store .wim

    💻 Usage Instructions

    1. Save the script below as Create-RecoveryPartition.ps1
    2. Run PowerShell as Administrator
    3. Wait for all steps to complete — it may take several minutes
    4. Verify recovery status using:
    reagentc /info
    

    📝 PowerShell Script

    # === Config ===
    $WimName      = "CustomRefresh"
    $ImageName    = "Custom Windows Image"
    $ImageIndex   = 1
    $CapturePath  = "C:\"
    $TempWim      = "C:\Recovery\$WimName.wim"
    $RecFolder    = "Recovery\WindowsRE"
    $VolumeLabel  = "Recovery"
    $TempLetter   = "R"
    $DiskNumber   = 0   # <-- Adjust if needed (0 is usually system disk)
    
    # === Check Admin Privileges ===
    if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Administrator")) {
        Write-Error "Run this script as Administrator."
        exit 1
    }
    
    # === Create folder if needed ===
    if (!(Test-Path "C:\Recovery")) {
        New-Item -ItemType Directory -Path "C:\Recovery" | Out-Null
    }
    
    # === Capture current C: as WIM image ===
    Write-Host "📦 Capturing system image to $TempWim ..."
    $dismCmd = "dism /Capture-Image /ImageFile:`"$TempWim`" /CaptureDir:C:\ /Name:`"$ImageName`""
    Start-Process -Wait -FilePath cmd.exe -ArgumentList "/c $dismCmd"
    
    if (!(Test-Path $TempWim)) {
        Write-Error "DISM capture failed. Exiting."
        exit 1
    }
    
    # === Get Disk Size and Calculate Shrink Amount ===
    Write-Host "📊 Calculating 30% shrink from disk 0..."
    $vol = Get-Volume -DriveLetter C
    $disk = Get-Disk -Number $DiskNumber
    $shrinkSizeMB = [math]::Floor($disk.Size / 1024 / 1024 * 0.3)
    
    # === Create DiskPart script ===
    $diskpartScript = @"
    select disk $DiskNumber
    list volume
    select volume $($vol.UniqueId)
    shrink desired=$shrinkSizeMB
    create partition primary
    format fs=ntfs quick label=$VolumeLabel
    assign letter=$TempLetter
    set id=de94bba4-06d1-4d40-a16a-bfd50179d6ac
    gpt attributes=0x8000000000000001
    exit
    "@
    
    $scriptPath = "$env:TEMP\diskpart_script.txt"
    $diskpartScript | Set-Content -Path $scriptPath -Encoding ASCII
    Start-Process -Wait -FilePath diskpart -ArgumentList "/s `"$scriptPath`""
    Remove-Item $scriptPath
    
    # === Copy image to recovery partition ===
    $targetFolder = "$($TempLetter):\$RecFolder"
    Write-Host "📁 Copying image to $targetFolder ..."
    New-Item -ItemType Directory -Path $targetFolder -Force | Out-Null
    Copy-Item -Path $TempWim -Destination "$targetFolder\CustomRefresh.wim"
    
    # === Register custom image ===
    Write-Host "🛠️ Configuring recovery image with reagentc..."
    Start-Process -Wait -FilePath cmd.exe -ArgumentList "/c reagentc /disable"
    Start-Process -Wait -FilePath cmd.exe -ArgumentList "/c reagentc /SetOSImage /Path $TempLetter`:\$RecFolder /Index $ImageIndex"
    Start-Process -Wait -FilePath cmd.exe -ArgumentList "/c reagentc /enable"
    
    # === Show status ===
    Write-Host "`n📋 Recovery configuration:"
    Start-Process -Wait -FilePath cmd.exe -ArgumentList "/c reagentc /info"
    
    # === Hide the recovery partition ===
    Write-Host "🔒 Hiding recovery partition..."
    $removeScript = @"
    select volume $TempLetter
    remove letter=$TempLetter
    exit
    "@
    $removePath = "$env:TEMP\remove_letter.txt"
    $removeScript | Set-Content -Path $removePath -Encoding ASCII
    Start-Process -Wait -FilePath diskpart -ArgumentList "/s `"$removePath`""
    Remove-Item $removePath
    
    Write-Host "`n✅ Done! Recovery partition created and configured." -ForegroundColor Green
    

    🔁 Result

    Once the script is run, your system will be configured to use your captured recovery image when you choose:

    Settings > System > Recovery > Reset this PC > Keep my files


    📌 Notes

    • Only use on stable systems (tested and configured).
    • Always keep a backup before modifying partitions.
    • You can optionally place the .wim on a USB or external disk for reuse on other machines.

    🔧 Bootable Recovery USB (Optional)

    To create a bootable USB containing the recovery image and tools:

    🛠️ Steps

    1. Insert a USB drive (8 GB or larger)

    2. Format the USB as FAT32 or NTFS

    3. Mount a Windows ISO (same version as your current OS) or download the Windows ADK to access WinPE tools.

    4. Create WinPE boot environment (if not using ISO):

    # From Deployment and Imaging Tools Environment (as Admin)
    copype amd64 C:\WinPE_amd64
    MakeWinPEMedia /UFD C:\WinPE_amd64 E:
    
    1. Copy your custom .wim file to the USB:
    # Assuming CustomRefresh.wim exists
    Copy-Item C:\Recovery\CustomRefresh.wim E:\sources\install.wim
    
    1. Make USB bootable (if not already via MakeWinPEMedia). For ISO-based bootable USB, use Rufus to burn the Windows ISO and then replace the install.wim.

    2. Boot into USB and use DISM or Recovery Environment to apply the image manually.

    ✅ Apply WIM manually (example):

    # In Windows PE command prompt:
    dism /Apply-Image /ImageFile:E:\sources\install.wim /Index:1 /ApplyDir:C:\
    

  • Inspection sécurisée de liens suspects avec Application Guard

    🔐 Inspection sécurisée de liens suspects avec Application Guard et notepad.libreops.cc

    🎯 Objectif

    Analyser des liens suspects provenant d’e-mails ou de messages en toute sécurité, en procédant ainsi :

    1. Coller les liens dans un bloc-notes en ligne avant d’entrer dans le bac à sable.
    2. Ouvrir Microsoft Edge avec Application Guard.
    3. Accéder à la page du bloc-notes depuis Edge sécurisé pour copier les liens et les inspecter sans risque.

    🧰 Prérequis

    • Windows 10/11 Professionnel ou Entreprise
    • Microsoft Defender Application Guard activé :
      • Ouvrir optionalfeatures.exe → cocher Microsoft Defender Application Guard
      • Redémarrer si demandé

    🌐 Étapes à suivre

    🟢 Étape 1 : Ouvrir notepad.libreops.cc dans votre navigateur principal

    1. Rendez-vous sur : https://notepad.libreops.cc
    2. Collez le ou les liens suspects dans la note.
    3. Cliquez sur « Save » (en haut à droite).
    4. Copiez l’URL unique de la note (ex. : https://notepad.libreops.cc/xyz123abc456)

    💡 Vous utiliserez cette URL dans le navigateur sécurisé pour accéder aux liens.


    🔵 Étape 2 : Ouvrir Microsoft Edge avec Application Guard

    1. Lancez Microsoft Edge.
    2. Cliquez sur le menu à trois points → Nouvelle fenêtre Application Guard. image

    🔐 Cela ouvre une session de navigation hautement isolée.


    🔍 Étape 3 : Accéder à votre note depuis Edge sécurisé

    1. Dans la fenêtre Application Guard, ouvrez l’URL copiée à l’étape 1
      (ex. : https://notepad.libreops.cc/xyz123abc456)

    2. Une fois la note ouverte :
      • Copiez le lien suspect depuis la note.
      • Collez-le dans la barre d’adresse ou survolez-le pour l’analyser.
    3. Vous pouvez maintenant :
      • Vérifier visuellement le lien.
      • L’ouvrir uniquement si nécessaire.
      • Utiliser des outils d’analyse depuis Application Guard :

    🧽 Après utilisation

    • Fermez la fenêtre Application Guard pour détruire la session.

  • How to Configure network printer on HMI

    Setting Up a Network Printer on HMI

    Source : Link

    • Our architecture is as below: image

    Instructions: Printing via network

    • Call up the control Panel. • Open the “Printer Properties” dialog with the “Printer” icon.

    – Select the printer type “PCL Laser” under “Printer Language”.

    – Select the interface “PrintServer” under “Port”.

    – Enter the network address of the print server under “IP:Port:”.

    Note: Many printers have an integrated “print server”. This allows you to specify the address of the printer directly.

    Note the correct notation -> “IP address:port number” (colon between the IP address and port number (Link)) Example: 172.16.34.30:9100

    – Select the paper size in the “Paper Size” drop-down box.

    – Specify the orientation of the printout under “Orientation”: “Portrait” for vertical format “Landscape” for horizontal format

    – Select the print quality:

    To print in draft quality, select “Draft Mode”.

    To print in color, select “Color”.

    – Confirm the entries with “OK”.

    image

    • make sure the network configuration of the HMI is from DHCP
    • Note: if IP is set manually , do not forget to fill in the default gateway address.
  • Docker Overlay Cleanup Script

    Docker Overlay Cleanup Script

    Here is a script to help clean up Docker overlay files on an Ubuntu system running in Proxmox.

    #!/bin/bash
    
    # Define the base directory for Docker overlay files
    OVERLAY_BASE_DIR="/var/lib/docker/overlay2"
    
    # Loop through each overlay directory
    find "$OVERLAY_BASE_DIR" -type d -name "diff" | while read -r diff_dir; do
        # Define the tmp directory within each diff directory
        tmp_dir="$diff_dir/tmp"
        
        # Check if the tmp directory exists
        if [ -d "$tmp_dir" ]; then
            echo "Cleaning up temporary files in $tmp_dir"
    
            # Remove files and directories within tmp that are older than 1 day
            find "$tmp_dir" -mindepth 1 -mtime +1 -exec rm -rf {} +
        fi
    done
    
    echo "Cleanup complete."
    

    Explanation:

    • Base Directory: The script targets /var/lib/docker/overlay2, where Docker stores its overlay files.

    • Loop through Directories: It searches for directories named diff within the overlay directories. Each diff directory represents the writable layer of a Docker container.

    • Temporary Files: Within each diff directory, it looks for a tmp directory, which is typically used for temporary files.

    • Cleanup: It removes files and directories within tmp that are older than 1 day. You can adjust the -mtime +1 parameter to change the age threshold for files to be deleted.

    Important Considerations:

    • Backup: Always ensure you have backups of important data before running cleanup scripts.

    • Running Containers: Be cautious with running containers. Cleaning up files in use by active containers can cause issues.

    • Permissions: Ensure the script is run with appropriate permissions. You might need to run it with sudo.

    • Testing: Test the script in a safe environment before applying it to a production system.

    You can save this script to a file, for example, clean_docker_overlay.sh, make it executable with chmod +x clean_docker_overlay.sh, and run it with sudo ./clean_docker_overlay.sh. Adjust the script as necessary to fit your specific needs and environment.

  • Dual boot to VHDX without USB

    Automated Windows 11 Tiny Core VHDX Creator Script

    Create a lightweight Windows 11 installation in a virtual disk (VHDX) file with minimal user intervention using this comprehensive PowerShell automation script.

    Overview

    This script automates the process of creating a Windows 11 Tiny Core installation inside a VHDX file, making it perfect for virtualization, testing, or creating portable Windows environments. The process is divided into two clear phases with minimal manual intervention required.

    Features

    • Fully Automated VHDX Creation - Creates, formats, and mounts virtual disk
    • Automatic Tiny11 Builder Integration - Downloads and runs Tiny11 Builder automatically
    • Custom Boot Menu Names - Add personalized boot entry names
    • Interactive Image Selection - Choose specific Windows 11 editions
    • Two-Phase Process - Clear separation between ISO creation and disk operations
    • Comprehensive Error Handling - Detailed feedback and error management
    • Flexible Configuration - Customizable paths, sizes, and names

    Prerequisites

    Before running the script, ensure you have:

    • Administrator privileges (required for diskpart and DISM operations)
    • Windows 11 ISO file downloaded from Microsoft
    • Sufficient disk space (at least 60GB recommended)
    • PowerShell 5.1 or later

    Quick Start

    Basic Usage

    # Run with default settings
    .\Win11TinyVHDX.ps1
    

    Advanced Usage

    # Custom configuration
    .\Win11TinyVHDX.ps1 -VHDXPath "D:\VMs\MyTinyWin11.vhdx" -VHDXSizeGB 40 -VHDXLabel "TinyWindows" -BootEntryName "Win11-Custom-Build"
    

    Script Parameters

    Parameter Default Value Description
    VHDXPath E:\VHD_Store\Win11tinycore.vhdx Full path for the VHDX file
    VHDXSizeGB 50 Size of the virtual disk in GB
    VHDXLabel Win11tinycore Volume label for the virtual disk
    BootEntryName Win11TinyCore-Master Custom name in boot menu

    Process Flow

    Phase 1: Tiny11 Core ISO Creation 🔨

    1. Mount Windows 11 ISO (Manual)
      • Download Windows 11 ISO from Microsoft
      • Right-click → Mount or use Windows Explorer
    2. Specify ISO Drive Letter (Interactive)
      • Script displays available drives
      • Enter the drive letter where ISO is mounted
    3. Download Tiny11 Builder (Automated)
      • Automatically downloads from GitHub repository
      • Extracts and prepares the builder tool
    4. Create Tiny11 ISO (Automated)
      • Runs Tiny11 Builder automatically
      • Creates optimized Windows 11 ISO
    5. Mount Tiny11 ISO (Manual)
      • Mount the newly created Tiny11 ISO file

    Phase 2: Virtual Disk Creation & Image Application 💾

    1. Create VHDX Virtual Disk (Automated)
      • Uses diskpart to create expandable VHDX
      • Formats and assigns drive letter
    2. Select Windows Edition (Interactive)
      • Displays available Windows editions
      • Choose the desired edition index
    3. Apply Windows Image (Automated)
      • Uses DISM to install Windows to VHDX
      • Applies selected edition
    4. Configure Boot Manager (Automated)
      • Sets up boot configuration
      • Adds custom boot menu entry

    Sample Output

    === Windows 11 Tiny Core VHDX Creator ===
    This script will create a VHDX and install Windows 11 Tiny Core
    
    === Phase 1: Creating Tiny11 Core ISO ===
    
    Step 1: Manual steps required:
    1. Download Windows 11 ISO file
    2. Mount the Windows 11 ISO (right-click -> Mount)
    
    Step 2: Enter the drive letter where Windows 11 ISO is mounted:
    Available drives:
      D: - Windows_11_ISO
    Enter drive letter (e.g., D): D
    
    Step 3: Downloading Tiny11 Builder...
    Tiny11 Builder downloaded successfully!
    
    Step 4: Running Tiny11 Builder automatically...
    Executing Tiny11 Builder...
    Source ISO: D:\
    Tiny11 Builder completed successfully!
    
    ===============================================
        TINY11 CORE ISO CREATION COMPLETED!
    ===============================================
    
    NEXT PHASE INFORMATION:
    The script will now proceed to:
      • Create a virtual disk (VHDX) file
      • Format and prepare the virtual disk
      • Apply the Tiny11 Core image to the virtual disk
      • Configure the boot manager for the installation
    
    Virtual Disk Details:
      Path: E:\VHD_Store\Win11tinycore.vhdx
      Size: 50 GB
      Label: Win11tinycore
      Boot Entry: Win11TinyCore-Master
    
    Do you want to proceed with virtual disk creation and image application? (y/n): y
    
    === Phase 2: Creating Virtual Disk and Applying Image ===
    
    Step 7: Creating VHDX...
    VHDX created successfully and mounted as drive X:
    
    Step 8: Getting Windows image information...
    [WIM Edition List Displayed]
    Enter index number: 6
    
    Step 9: Applying Windows image...
    Windows image applied successfully!
    
    Step 10: Configuring boot manager...
    Boot manager configured successfully with custom name!
    
    === Installation Complete! ===
    

    Technical Details

    VHDX Creation Process

    The script uses diskpart commands to:

    create vdisk file="path" maximum=size type=expandable
    attach vdisk
    create part primary
    format quick label="label"
    assign letter=X
    

    Image Application

    Uses DISM (Deployment Image Servicing and Management) tool:

    dism /apply-image /imagefile:source.wim /index:N /applydir:X:\
    

    Boot Configuration

    Configures Windows Boot Manager with custom entry:

    bcdboot X:\windows /d "Custom Boot Name"
    

    Creating Child Vms with differencing disk

    New-VHD -ParentPath 'E:\VHD_Store\Win11tinycore.vhdx' -Path 'E:\VHD_Store\Android.vhdx' -Differencing
    

    Note:

    • Use the command above to create a differencing VHD. Then, use EasyBCD to add the newly created VHDX to the boot menu. Ensure that the Hyper-V Virtual Machine Management service is running.
    • Before creating the differencing disk, ensure that you have installed all necessary drivers and essential software on the parent VM. This will prevent the need to reinstall them on the child VMs.
    • Do not update the parent VM under any circumstances, as this could render the child VMs unbootable. Make sure not to boot into the parent VM after creating child vms.

    Use Cases

    1. Virtual Machine Development

    • Create lightweight VMs for testing
    • Rapid deployment of clean Windows environments
    • Resource-efficient virtualization

    2. System Recovery

    • Portable Windows environment on external drive
    • Emergency boot scenarios
    • System repair and maintenance

    3. Educational Purposes

    • Learning Windows internals
    • Understanding deployment processes
    • Teaching virtualization concepts

    4. Software Testing

    • Isolated testing environments
    • Clean OS installations for software validation
    • Multiple Windows configurations

    Benefits of Tiny11 Core

    • Reduced Size: ~2-3GB smaller than standard Windows 11
    • Faster Boot: Optimized startup performance
    • Lower Resource Usage: Minimal background processes
    • Essential Features: Keeps core Windows functionality
    • Compatibility: Full Windows 11 application support

    Troubleshooting

    Common Issues

    Script requires Administrator privileges

    Solution: Right-click PowerShell → "Run as Administrator"
    

    VHDX creation fails

    Solution: Ensure sufficient disk space and valid path
    Check: Disk permissions and available storage
    

    Tiny11 Builder download fails

    Solution: Check internet connection and GitHub accessibility
    Alternative: Download manually from https://github.com/ntdevlabs/tiny11builder
    

    Image application takes too long

    Solution: Normal behavior - DISM operations can take 15-30 minutes
    Monitor: Task Manager for disk activity
    

    Boot configuration fails

    Solution: Ensure Windows image was applied successfully
    Check: X:\windows directory exists and contains system files
    

    Performance Tips

    • Use SSD storage for faster VHDX operations
    • Allocate sufficient RAM during image application
    • Close unnecessary applications to free up system resources
    • Use wired network connection for downloads

    Security Considerations

    • Run from trusted sources - Verify script integrity
    • Administrator privileges - Understand elevation requirements
    • Antivirus exclusions - May need temporary exclusions for VHDX operations
    • Network downloads - Ensure secure connection for Tiny11 Builder

    Advanced Configuration

    Custom VHDX Locations

    # Network storage
    .\Win11TinyVHDX.ps1 -VHDXPath "\\server\vms\tiny11.vhdx"
    
    # External drive
    .\Win11TinyVHDX.ps1 -VHDXPath "F:\VirtualMachines\Win11Tiny.vhdx"
    

    Multiple Configurations

    # Development environment
    .\Win11TinyVHDX.ps1 -BootEntryName "Win11-Dev-Environment" -VHDXSizeGB 60
    
    # Testing environment  
    .\Win11TinyVHDX.ps1 -BootEntryName "Win11-Test-Lab" -VHDXSizeGB 40
    

    Script Download

    The complete PowerShell script is available below. Save it as Win11TinyVHDX.ps1 and run with Administrator privileges.

    Note: Ensure execution policy allows script execution:

    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
    
    # Windows 11 Tiny Core VHDX Creator Script
    # Requires Administrator privileges
    
    param(
        [string]$VHDXPath = "E:\VHD_Store\Win11tinycore.vhdx",
        [int]$VHDXSizeGB = 50,
        [string]$VHDXLabel = "Win11tinycore",
        [string]$BootEntryName = "Win11TinyCore-Master"
    )
    
    # Check if running as administrator
    if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
        Write-Host "This script requires Administrator privileges. Please run as Administrator." -ForegroundColor Red
        exit 1
    }
    
    Write-Host "=== Windows 11 Tiny Core VHDX Creator ===" -ForegroundColor Cyan
    Write-Host "This script will create a VHDX and install Windows 11 Tiny Core" -ForegroundColor Yellow
    Write-Host ""
    
    # Function to create VHDX using diskpart
    function Create-VHDX {
        param(
            [string]$Path,
            [int]$SizeMB,
            [string]$Label,
            [string]$DriveLetter = "X"
        )
        
        Write-Host "Creating VHDX file at: $Path" -ForegroundColor Green
        
        # Ensure directory exists
        $directory = Split-Path $Path -Parent
        if (!(Test-Path $directory)) {
            New-Item -ItemType Directory -Path $directory -Force | Out-Null
        }
        
        # Create diskpart script
        $diskpartScript = @"
    create vdisk file="$Path" maximum=$SizeMB type=expandable
    attach vdisk
    create part primary
    format quick label="$Label"
    assign letter=$DriveLetter
    exit
    "@
        
        $scriptPath = "$env:TEMP\create_vhdx.txt"
        $diskpartScript | Out-File -FilePath $scriptPath -Encoding ASCII
        
        # Run diskpart
        Write-Host "Running diskpart to create and format VHDX..." -ForegroundColor Yellow
        diskpart /s $scriptPath
        
        # Clean up
        Remove-Item $scriptPath -Force
        
        if (Test-Path "${DriveLetter}:\") {
            Write-Host "VHDX created successfully and mounted as drive ${DriveLetter}:" -ForegroundColor Green
            return $true
        } else {
            Write-Host "Failed to create or mount VHDX" -ForegroundColor Red
            return $false
        }
    }
    
    # Function to download Tiny11 Builder
    function Download-Tiny11Builder {
        $downloadPath = "$env:TEMP\tiny11builder"
        $zipPath = "$downloadPath\tiny11builder.zip"
        
        Write-Host "Downloading Tiny11 Builder..." -ForegroundColor Yellow
        
        # Create download directory
        if (!(Test-Path $downloadPath)) {
            New-Item -ItemType Directory -Path $downloadPath -Force | Out-Null
        }
        
        try {
            # Download the repository
            $url = "https://github.com/ntdevlabs/tiny11builder/archive/refs/heads/main.zip"
            Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
            
            # Extract the zip
            Expand-Archive -Path $zipPath -DestinationPath $downloadPath -Force
            
            # Find the PowerShell script
            $scriptPath = Get-ChildItem -Path $downloadPath -Name "tiny11Coremaker.ps1" -Recurse | Select-Object -First 1
            
            if ($scriptPath) {
                $fullScriptPath = Join-Path $downloadPath $scriptPath
                Write-Host "Tiny11 Builder downloaded to: $fullScriptPath" -ForegroundColor Green
                return $fullScriptPath
            } else {
                Write-Host "Could not find tiny11Coremaker.ps1 in downloaded files" -ForegroundColor Red
                return $null
            }
        }
        catch {
            Write-Host "Failed to download Tiny11 Builder: $($_.Exception.Message)" -ForegroundColor Red
            return $null
        }
    }
    
    # Function to get Windows 11 ISO drive letter
    function Get-ISODriveLetter {
        param([string]$Prompt)
        
        Write-Host $Prompt -ForegroundColor Yellow
        Write-Host "Available drives:" -ForegroundColor Cyan
        Get-WmiObject -Class Win32_LogicalDisk | Where-Object { $_.DriveType -eq 5 } | ForEach-Object {
            Write-Host "  $($_.DeviceID) - $($_.VolumeName)" -ForegroundColor White
        }
        
        do {
            $driveLetter = Read-Host "Enter drive letter (e.g., D)"
            $driveLetter = $driveLetter.TrimEnd(':').ToUpper()
            
            if ($driveLetter -match "^[A-Z]$" -and (Test-Path "${driveLetter}:\")) {
                return $driveLetter
            } else {
                Write-Host "Invalid drive letter or drive not found. Please try again." -ForegroundColor Red
            }
        } while ($true)
    }
    
    # Function to get WIM info and let user select index
    function Get-WIMInfo {
        param([string]$ISODriveLetter)
        
        $wimFile = "${ISODriveLetter}:\sources\install.wim"
        
        if (!(Test-Path $wimFile)) {
            Write-Host "install.wim not found at $wimFile" -ForegroundColor Red
            return $null
        }
        
        Write-Host "Getting Windows image information..." -ForegroundColor Yellow
        Write-Host ""
        
        try {
            # Get WIM info
            $wimInfo = dism /get-wiminfo /wimfile:$wimFile
            $wimInfo | Write-Host
            
            Write-Host ""
            Write-Host "Please select the Windows edition index from the list above:" -ForegroundColor Yellow
            
            do {
                $index = Read-Host "Enter index number"
                if ($index -match "^\d+$" -and [int]$index -gt 0) {
                    return [int]$index
                } else {
                    Write-Host "Please enter a valid index number." -ForegroundColor Red
                }
            } while ($true)
        }
        catch {
            Write-Host "Failed to get WIM information: $($_.Exception.Message)" -ForegroundColor Red
            return $null
        }
    }
    
    # Function to apply Windows image
    function Apply-WindowsImage {
        param(
            [string]$ISODriveLetter,
            [int]$Index,
            [string]$TargetDrive = "X"
        )
        
        $wimFile = "${ISODriveLetter}:\sources\install.wim"
        $targetPath = "${TargetDrive}:\"
        
        Write-Host "Applying Windows image..." -ForegroundColor Yellow
        Write-Host "Source: $wimFile" -ForegroundColor Cyan
        Write-Host "Index: $Index" -ForegroundColor Cyan
        Write-Host "Target: $targetPath" -ForegroundColor Cyan
        Write-Host ""
        
        try {
            dism /apply-image /imagefile:$wimFile /index:$Index /applydir:$targetPath
            
            if ($LASTEXITCODE -eq 0) {
                Write-Host "Windows image applied successfully!" -ForegroundColor Green
                return $true
            } else {
                Write-Host "Failed to apply Windows image (Exit code: $LASTEXITCODE)" -ForegroundColor Red
                return $false
            }
        }
        catch {
            Write-Host "Error applying Windows image: $($_.Exception.Message)" -ForegroundColor Red
            return $false
        }
    }
    
    # Function to configure boot manager
    function Configure-BootManager {
        param(
            [string]$TargetDrive = "X",
            [string]$BootEntryName = "Win11TinyCore-Master"
        )
        
        Write-Host "Configuring boot manager..." -ForegroundColor Yellow
        Write-Host "Boot entry name: $BootEntryName" -ForegroundColor Cyan
        
        try {
            # Use /d parameter to set custom boot menu description
            bcdboot "${TargetDrive}:\windows" /d "$BootEntryName"
            
            if ($LASTEXITCODE -eq 0) {
                Write-Host "Boot manager configured successfully with custom name!" -ForegroundColor Green
                return $true
            } else {
                Write-Host "Failed to configure boot manager (Exit code: $LASTEXITCODE)" -ForegroundColor Red
                return $false
            }
        }
        catch {
            Write-Host "Error configuring boot manager: $($_.Exception.Message)" -ForegroundColor Red
            return $false
        }
    }
    
    # Main script execution
    try {
        Write-Host "=== Phase 1: Creating Tiny11 Core ISO ===" -ForegroundColor Magenta
        Write-Host ""
        
        Write-Host "Step 1: Manual steps required:" -ForegroundColor Magenta
        Write-Host "1. Download Windows 11 ISO file" -ForegroundColor Yellow
        Write-Host "2. Mount the Windows 11 ISO (right-click -> Mount)" -ForegroundColor Yellow
        Write-Host ""
        
        # Step 2: Get Windows 11 ISO drive letter
        $win11Drive = Get-ISODriveLetter -Prompt "Step 2: Enter the drive letter where Windows 11 ISO is mounted:"
        
        # Step 3: Download Tiny11 Builder
        Write-Host ""
        Write-Host "Step 3: Downloading Tiny11 Builder..." -ForegroundColor Magenta
        $tiny11Script = Download-Tiny11Builder
        
        if (!$tiny11Script) {
            throw "Failed to download Tiny11 Builder"
        }
        
        Write-Host ""
        Write-Host "Step 4: Running Tiny11 Builder automatically..." -ForegroundColor Magenta
        
        # Change to the Tiny11 Builder directory
        $tiny11Dir = Split-Path $tiny11Script -Parent
        Set-Location $tiny11Dir
        
        Write-Host "Executing Tiny11 Builder..." -ForegroundColor Yellow
        Write-Host "Source ISO: ${win11Drive}:\" -ForegroundColor Cyan
        Write-Host ""
        
        try {
            # Run the Tiny11 Builder script with the Windows 11 ISO path
            & $tiny11Script
            
            if ($LASTEXITCODE -eq 0) {
                Write-Host "Tiny11 Builder completed successfully!" -ForegroundColor Green
            } else {
                Write-Host "Tiny11 Builder finished with exit code: $LASTEXITCODE" -ForegroundColor Yellow
            }
        }
        catch {
            Write-Host "Error running Tiny11 Builder: $($_.Exception.Message)" -ForegroundColor Red
            throw "Failed to run Tiny11 Builder"
        }
        
        Write-Host ""
        Write-Host "Step 5: Manual step required:" -ForegroundColor Magenta
        Write-Host "Please mount the created Tiny11 ISO file" -ForegroundColor Yellow
        Write-Host "The ISO should be located in: $tiny11Dir" -ForegroundColor Cyan
        Write-Host ""
        
        Read-Host "Press Enter when you have mounted the Tiny11 ISO..."
        
        # Step 6: Get Tiny11 ISO drive letter
        $tiny11Drive = Get-ISODriveLetter -Prompt "Step 6: Enter the drive letter where Tiny11 ISO is mounted:"
        
        Write-Host ""
        Write-Host "===============================================" -ForegroundColor Green
        Write-Host "    TINY11 CORE ISO CREATION COMPLETED!" -ForegroundColor Green
        Write-Host "===============================================" -ForegroundColor Green
        Write-Host ""
        Write-Host "NEXT PHASE INFORMATION:" -ForegroundColor Yellow
        Write-Host "The script will now proceed to:" -ForegroundColor White
        Write-Host "  • Create a virtual disk (VHDX) file" -ForegroundColor Cyan
        Write-Host "  • Format and prepare the virtual disk" -ForegroundColor Cyan
        Write-Host "  • Apply the Tiny11 Core image to the virtual disk" -ForegroundColor Cyan
        Write-Host "  • Configure the boot manager for the installation" -ForegroundColor Cyan
        Write-Host ""
        Write-Host "Virtual Disk Details:" -ForegroundColor Yellow
        Write-Host "  Path: $VHDXPath" -ForegroundColor White
        Write-Host "  Size: $VHDXSizeGB GB" -ForegroundColor White
        Write-Host "  Label: $VHDXLabel" -ForegroundColor White
        Write-Host "  Boot Entry: $BootEntryName" -ForegroundColor White
        Write-Host ""
        
        $continue = Read-Host "Do you want to proceed with virtual disk creation and image application? (y/n)"
        if ($continue -ne 'y' -and $continue -ne 'Y') {
            Write-Host "Script terminated by user." -ForegroundColor Yellow
            exit 0
        }
        
        Write-Host ""
        Write-Host "=== Phase 2: Creating Virtual Disk and Applying Image ===" -ForegroundColor Magenta
        Write-Host ""
        
        # Step 7: Create VHDX
        Write-Host "Step 7: Creating VHDX..." -ForegroundColor Magenta
        $vhdxSizeMB = $VHDXSizeGB * 1024
        if (!(Create-VHDX -Path $VHDXPath -SizeMB $vhdxSizeMB -Label $VHDXLabel)) {
            throw "Failed to create VHDX"
        }
        
        # Step 8: Get WIM info and select index
        Write-Host ""
        Write-Host "Step 8: Getting Windows image information..." -ForegroundColor Magenta
        $selectedIndex = Get-WIMInfo -ISODriveLetter $tiny11Drive
        
        if (!$selectedIndex) {
            throw "Failed to get WIM information or select index"
        }
        
        # Step 9: Apply Windows image
        Write-Host ""
        Write-Host "Step 9: Applying Windows image..." -ForegroundColor Magenta
        if (!(Apply-WindowsImage -ISODriveLetter $tiny11Drive -Index $selectedIndex)) {
            throw "Failed to apply Windows image"
        }
        
        # Step 10: Configure boot manager
        Write-Host ""
        Write-Host "Step 10: Configuring boot manager..." -ForegroundColor Magenta
        if (!(Configure-BootManager -BootEntryName $BootEntryName)) {
            throw "Failed to configure boot manager"
        }
        
        # Success message
        Write-Host ""
        Write-Host "=== Installation Complete! ===" -ForegroundColor Green
        Write-Host "Windows 11 Tiny Core has been successfully installed to VHDX:" -ForegroundColor Green
        Write-Host $VHDXPath -ForegroundColor Cyan
        Write-Host ""
        Write-Host "You can now:" -ForegroundColor Yellow
        Write-Host "1. Detach the VHDX: diskpart -> select vdisk file=$VHDXPath -> detach vdisk" -ForegroundColor White
        Write-Host "2. Use the VHDX with Hyper-V or other virtualization software" -ForegroundColor White
        Write-Host "3. Boot from the VHDX on physical hardware (advanced)" -ForegroundColor White
        
    }
    catch {
        Write-Host ""
        Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
        Write-Host "Installation failed. Please check the error message above." -ForegroundColor Red
        exit 1
    }
    
    # Optional: Detach VHDX
    Write-Host ""
    $detach = Read-Host "Would you like to detach the VHDX now? (y/n)"
    if ($detach -eq 'y' -or $detach -eq 'Y') {
        Write-Host "Detaching VHDX..." -ForegroundColor Yellow
        
        $detachScript = @"
    select vdisk file="$VHDXPath"
    detach vdisk
    exit
    "@
        
        $scriptPath = "$env:TEMP\detach_vhdx.txt"
        $detachScript | Out-File -FilePath $scriptPath -Encoding ASCII
        diskpart /s $scriptPath
        Remove-Item $scriptPath -Force
        
        Write-Host "VHDX detached successfully!" -ForegroundColor Green
    }
    

    Conclusion

    This automated script streamlines the creation of Windows 11 Tiny Core VHDX files, making it accessible for both beginners and advanced users. The two-phase approach provides clear checkpoints and flexibility, while the comprehensive error handling ensures a smooth experience.

    Whether you’re creating virtual machines, building portable Windows environments, or setting up testing scenarios, this script provides a robust and efficient solution for your Windows 11 deployment needs.


    Last updated: June 2025 Compatible with: Windows 11, PowerShell 5.1+ Requirements: Administrator privileges, Windows 11 ISO

  • Extend Logical Volume in Ubuntu

    Steps to Extend Logical Volume in Ubuntu

    This guide outlines the steps to extend a logical volume in Ubuntu to utilize the full disk space.

    Prerequisites

    • Access to a terminal with sudo privileges
    • cloud-guest-utils package installed (for growpart)

    Steps

    Step 1: Check the Disk Layout

    Verify the current disk layout to ensure that the additional space is available and correctly recognized by the system.

    sudo fdisk -l /dev/sda
    

    Step 2: Check Volume Group

    Check the volume group to see how much free space is available.

    sudo vgdisplay
    

    Step 3: Install growpart

    If growpart is not installed, install it using the following commands:

    sudo apt update
    sudo apt install cloud-guest-utils
    

    Step 4: Extend the Partition

    Use growpart to extend the partition to include the new disk space. Replace /dev/sda and 3 with your disk and partition number if different.

    sudo growpart /dev/sda 3
    

    Step 5: Resize the Physical Volume

    Resize the physical volume to include the new space:

    sudo pvresize /dev/sda3
    

    Step 6: Extend the Logical Volume

    Extend the logical volume to use all available free space in the volume group. Replace /dev/ubuntu-vg/ubuntu-lv with your logical volume path if different.

    sudo lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv
    

    Step 7: Resize the Filesystem

    Resize the filesystem to utilize the new space. For an ext4 filesystem, use:

    sudo resize2fs /dev/ubuntu-vg/ubuntu-lv
    

    Step 8: Verify the Changes

    Check the disk space again to ensure that the changes have taken effect:

    df -h
    

    Conclusion

    By following these steps, you should be able to extend your logical volume to utilize the full disk space available.

  • Setting Up a Weekly Cron Job for Pi-hole Gravity Updates

    Setting Up a Weekly Cron Job for Pi-hole Gravity Updates

    In this blog post, I’ll walk you through the process of setting up a weekly cron job to send an SMS notification when the Pi-hole gravity update is completed. This setup is particularly useful if you’re running Pi-hole in a Docker container and want to ensure that your cron job persists across container restarts.

    Prerequisites

    • A running Pi-hole instance in a Docker container.
    • Access to the host machine where Docker is running.
    • Basic knowledge of shell scripting and cron jobs.

    Step 1: Create the Shell Script

    First, create a shell script on the host machine that will be executed by the cron job. This script will update the Pi-hole gravity database and send an SMS notification upon successful completion.

    1. Create the Script:

      Open a text editor and create a new file named pihole_update_gravity_with_sms.sh:

      sudo nano /usr/local/bin/pihole_update_gravity_with_sms.sh
      
    2. Add the Script Content:

      Copy and paste the following content into the file:

       #!/bin/bash
         
       # Path to the Pi-hole gravity update script
       GRAVITY_SCRIPT="docker exec pihole /usr/local/bin/pihole updateGravity"
         
       # SMS API credentials
       SMS_USER="USERNAME"
       SMS_PASS="APIKEY"
       SMS_MESSAGE="Pi-hole Gravity Update Completed Successfully."
         
       echo "Starting gravity update..."
         
       # Execute the gravity update
       \$GRAVITY_SCRIPT
         
       # Check if the gravity update was successful
       if [ \$? -eq 0 ]; then
           echo "Gravity update completed successfully."
         
           # Send notification via SMS using the confirmed working syntax
           echo "Sending SMS notification..."
           /usr/bin/curl "https://smsapi.free-mobile.fr/sendmsg?user=\${SMS_USER}&pass=\${SMS_PASS}&msg=\${SMS_MESSAGE}"
         
           echo "SMS notification sent successfully."
       else
           echo "Gravity update failed. No SMS notification sent."
       fi
      
    3. Make the Script Executable:

      Change the permissions of the script to make it executable:

       sudo chmod +x /usr/local/bin/pihole_update_gravity_with_sms.sh
      

    Step 2: Add the Script to the Host’s Cron Schedule

    Next, add the script to the host’s cron schedule to run it weekly at 5:30 AM every Monday.

    1. Edit the Crontab:

      Open the crontab file for editing:

       sudo crontab -e
      
    2. Add the Cron Job Entry:

      Add the following line to the crontab file to run the script at 5:30 AM every Monday:

       30 5 * * 1 /usr/local/bin/pihole_update_gravity_with_sms.sh
      
    3. Save and Exit:

      Save the changes and exit the crontab editor.

    Step 3: Verify the Cron Job

    To ensure that the cron job is set up correctly, you can manually trigger the script and check the logs for any output or errors.

    1. Manually Run the Script:

      Execute the script manually to verify that it works as expected:

       sudo /usr/local/bin/pihole_update_gravity_with_sms.sh
      
    2. Check the Logs:

      Review the system logs to see if the script was executed and if there were any errors:

      bash sudo tail -f /var/log/syslog

    Conclusion

    By following these steps, you have successfully set up a weekly cron job to send an SMS notification upon the completion of the Pi-hole gravity update. This setup ensures that you are promptly notified of the update status, helping you maintain the efficiency of your Pi-hole instance.

  • Automount Rclone Remote at System Startup

    Automount Rclone Remote at System Startup (Using systemd)

    To automount your Rclone remote at system startup, follow these steps:


    1. Create a systemd Service Unit

    Create a systemd unit file (example: /etc/systemd/system/rclone-debridlink.service).

    [Unit]
    Description=Rclone Mount for DebridLink Seedbox
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    Type=notify
    ExecStart=/usr/bin/rclone mount debridlink:/Seedbox /mnt/debridlink \\
      --allow-other \\
      --dir-cache-time 10s \\
      --vfs-cache-mode full \\
      --vfs-cache-max-size 500M \\
      --vfs-read-chunk-size 8M \\
      --vfs-read-chunk-size-limit 128M \\
      --vfs-fast-fingerprint \\
      --no-modtime \\
      --read-only \\
      --allow-non-empty \\
      --log-level INFO \\
      --log-file /var/log/rclone.log
    ExecStop=/bin/fusermount -u /mnt/debridlink
    Restart=on-failure
    User=<your-username>
    Group=<your-group>
    TimeoutSec=30
    
    [Install]
    WantedBy=multi-user.target
    

    Replace <your-username> and <your-group> with the appropriate values (you can find them using whoami and id -gn).


    2. Enable FUSE Permissions

    Ensure your user is part of the fuse group (if applicable):

    sudo usermod -aG fuse <your-username>
    

    Also install necessary FUSE packages (if missing):

    sudo apt install fuse3
    

    3. Enable and Start the Service

    Enable and start your systemd service:

    sudo systemctl daemon-reexec
    sudo systemctl daemon-reload
    sudo systemctl enable rclone-debridlink.service
    sudo systemctl start rclone-debridlink.service
    

    You can check the status with:

    systemctl status rclone-debridlink.service
    

    ✅ Your Rclone mount should now automatically mount on each system boot.

  • How to Set Up a Secure VPN Wi-Fi Access Point with OpenWRT and ProtonVPN (WireGuard)

    In this guide, we’ll walk through setting up an OpenWRT-based Wi-Fi access point that forces all connected clients to route their traffic through ProtonVPN using WireGuard. This ensures privacy, prevents DNS leaks, and blocks fallback to your ISP (kill switch).


    🔧 Prerequisites

    • OpenWRT-compatible router with internet access
    • OpenWRT firmware installed (with LuCI interface)
    • ProtonVPN account with WireGuard support

    1. Install Required Packages

    SSH into your OpenWRT router and run:

    opkg update
    opkg install luci-proto-wireguard wireguard-tools luci-app-wireguard kmod-wireguard
    

    2. Configure WireGuard Interface

    Use ProtonVPN’s WireGuard config generator and enter the config into:

    LuCI > Network > Interfaces > Add new interface

    • Name: wg0
    • Protocol: WireGuard VPN
    • Assign firewall zone: wgzone

    Add interface details from ProtonVPN:

    • Private key
    • Public key
    • Endpoint IP/port
    • Allowed IPs: 0.0.0.0/0
    • DNS server: 10.2.0.1 (or as provided)

    3. Create a VPN-Only Wi-Fi Network

    LuCI > Network > Wireless > Add

    • SSID: VPN-WiFi
    • Network: check vpnlan only

    Then go to Network > Interfaces > Add:

    • Name: vpnlan
    • Protocol: Static
    • IPv4 address: 192.168.100.1
    • Netmask: 255.255.255.0
    • Firewall zone: create new vpnlan

    Enable DHCP:
    Network > DHCP and DNS > Interfaces > vpnlan

    • Start: 100
    • Limit: 150
    • Lease time: 12h

    4. Configure Firewall Zones

    LuCI > Network > Firewall > Zones

    Create zones:

    • vpnlan: covers vpnlan network
      • Input: accept
      • Output: accept
      • Forward: reject
      • ✅ Masquerading
      • ✅ MSS Clamping
    • wgzone: covers wg0
      • Input: reject
      • Output: accept
      • Forward: reject
      • ✅ Masquerading

    Forwarding Rules:

    • Allow vpnlan ➝ wgzone
    • ❌ Do NOT allow vpnlan ➝ wan

    5. Force DNS Over VPN

    Network > DHCP and DNS:

    • ✅ Ignore resolv file
    • DNS Forwardings: 10.2.0.1 (or ProtonVPN DNS)

    Firewall > Traffic Rules:

    1. Allow DNS to VPN:

      • Source zone: vpnlan
      • Destination zone: wgzone
      • Destination IP: 10.2.0.1
      • Port: 53
      • ✅ Accept
    2. Block All Other DNS:

      • Source zone: vpnlan
      • Port: 53
      • ❌ Reject

    6. Test the Setup

    ✅ DNS Leak Test

    Connect to VPN-WiFi, visit https://dnsleaktest.com

    You should only see ProtonVPN DNS (e.g., 185.x.x.x in Netherlands).

    ✅ Kill Switch Test

    Temporarily bring down the VPN:

    ifdown wg0
    

    Clients on VPN-WiFi should lose all internet access.


    🔒 Result

    You now have a secure, VPN-only wireless access point. All connected devices are:

    • Protected via ProtonVPN (WireGuard)
    • Free of DNS leaks
    • Kill-switched from WAN fallback

    Enjoy secure browsing!

  • Network Drive Status CSC CACHE

    📁 Désactivation des fichiers hors connexion dans le Centre de synchronisation pour résoudre les conflits CSC CACHE

    📝 Problème

    Un lecteur réseau affiche le statut CSC CACHE, ce qui indique que les fichiers hors connexion sont activés via le Centre de synchronisation. Cela entraîne les problèmes suivants : image

    • Dossiers incomplets ou contenus manquants.
    • Conflits de fichiers ou fichiers obsolètes.
    • Impossibilité d’accéder à certains fichiers présents sur le serveur.

    ✅ Objectif

    Désactiver les fichiers hors connexion pour que le lecteur réseau reflète le contenu en temps réel sans mise en cache ni conflit.


    🔧 Étapes à suivre

    Étape 1 : Vérifier le statut dans le Centre de synchronisation

    1. Ouvrir le Panneau de configuration.
    2. Rechercher et ouvrir le Centre de synchronisation. image
    3. Dans le menu de gauche, cliquer sur Gérer les fichiers hors connexion.
    4. Vérifier le statut :
      • Si le message indique que les fichiers hors connexion sont activés, passer à l’étape suivante.

    Étape 2 : Désactiver les fichiers hors connexion

    1. Dans la fenêtre Fichiers hors connexion, cliquer sur Désactiver les fichiers hors connexion. image
    2. Cliquer sur OK.
    3. Redémarrer l’ordinateur lorsque cela est demandé.

    Étape 3 : Vider le cache CSC (optionnel mais recommandé)

    Si le lecteur réseau affiche toujours CSC CACHE ou si le problème persiste, il est conseillé de vider le cache des fichiers hors connexion.

    ⚠️ Attention : cette opération supprimera tous les fichiers en cache. S’assurer que toutes les modifications ont été synchronisées et sauvegardées.

    a. Modifier le registre pour vider le cache au redémarrage

    1. Appuyer sur Windows + R, taper regedit, puis appuyer sur Entrée.
    2. Naviguer jusqu’à la clé suivante :

      HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\CSC
      
    3. Créer ou modifier la valeur DWORD suivante :
      • Nom : FormatDatabase
      • Valeur : 1
    4. Fermer l’Éditeur du Registre et redémarrer l’ordinateur.

    Étape 4 : Vérifier l’accès au lecteur réseau

    Après redémarrage :

    1. Ouvrir l’Explorateur de fichiers.
    2. Accéder au lecteur réseau concerné.
    3. Vérifier que :
      • Le lecteur n’indique plus CSC CACHE.
      • Tous les fichiers et dossiers sont visibles et à jour.

  • Update Portainer on Ubuntu with Docker

    Update Portainer on Ubuntu with Docker

    This script stops and removes the existing Portainer container, pulls the latest image, and restarts Portainer using Docker.

    Script: update_portainer.sh

    #!/bin/bash
    
    # Update Portainer using Docker
    
    echo "Stopping existing Portainer container..."
    docker stop portainer
    
    echo "Removing existing Portainer container..."
    docker rm portainer
    
    echo "Pulling the latest Portainer image..."
    docker pull portainer/portainer-ce:lts
    
    echo "Restarting Portainer with the latest image..."
    docker run -d \
      -p 8000:8000 \
      -p 9443:9443 \
      --name portainer \
      --restart=always \
      -v /var/run/docker.sock:/var/run/docker.sock \
      -v portainer_data:/data \
      portainer/portainer-ce:lts
    
    echo "Portainer update complete."
    

    Make the Script Executable

    chmod +x update_portainer.sh
    

    Run the Script

    ./update_portainer.sh
    

    ⚠️ Adjust container name, ports, or volume paths if your setup differs.

  • Change the Metrics of the Network adapater

    Powershell script to change the metrics of the network adapter

    Some software, such as Siemens, installs virtual network adapters. By default, Windows automatically manages network metrics, which can cause delays during startup. In some cases, the system attempts to obtain a network address for the virtual adapter before the physical one, significantly delaying network availability for users. To address this issue, the following script manually sets the network metric values to prioritize the physical adapter.

    # Following pre requisites are required:
    # The network cards should be named properly 
    # Here in my scenario I have named the physical cards as Pedagogie which I could address it in the script.
    # Define the remote computer name
    $remotePC = "PC 01"
    
    # Define the adapter names and desired metric values
    $pedagogieAdapterName = "Pedagogie"
    $siemensAdapterName = "Ethernet 2"
    
    $pedagogieMetricValue = 1
    $siemensMetricValue = 100
    
    # Execute the following block remotely
    Invoke-Command -ComputerName $remotePC -ScriptBlock {
        param($pedagogieAdapterName, $siemensAdapterName, $pedagogieMetricValue, $siemensMetricValue)
    
        # Get the network adapter interfaces
        $pedagogieInterface = Get-NetAdapter -Name $pedagogieAdapterName -ErrorAction Stop
        $siemensInterface = Get-NetAdapter -Name $siemensAdapterName -ErrorAction Stop
    
        # Set the new metric values
        Set-NetIPInterface -InterfaceIndex $pedagogieInterface.ifIndex -InterfaceMetric $pedagogieMetricValue
        Set-NetIPInterface -InterfaceIndex $siemensInterface.ifIndex -InterfaceMetric $siemensMetricValue
    
        Write-Output "$comp : Metric for adapter '$pedagogieAdapterName' set to $pedagogieMetricValue."
        Write-Output "$comp : Metric for adapter '$siemensAdapterName' set to $siemensMetricValue."
    } -ArgumentList $pedagogieAdapterName, $siemensAdapterName, $pedagogieMetricValue, $siemensMetricValue
    
    
  • CRX-10 app not finding the Robot

    CRX-10 app not finding the Robot

  • Freemobile SMS API template

    ```bash

    SMS API credentials

    SMS_USER=”USERNAME” SMS_PASS=”APIKEY” SMS_MESSAGE=”Pi-hole Gravity Update Completed Successfully.”

  • MailMerge avec ThunderBird

    MailMerge avec Thunderbird

  • Nvidia Driver install

    Documentation: Clean Nvidia Driver Installation using DDU and NVCleanInstall

  • Chocolatey package automation via github workflow

    Chocolatey Package Deployment Workflow

  • Deploy the new Microsoft Teams client

    Source: link

  • Copy a desired profile to default profile

    Use defprof to copy the desired profile

  • Unable to update windows due to SSL error

    Source : https://learn.microsoft.com/en-us/answers/questions/1571547/error-connecting-to-update-service-80072f8f

  • Recover data using Duplicati

    Points to remember while recovering using Duplicati.

  • Mounting a CIFS Share in an Unprivileged LXC Container Running Docker

    This guide details how to mount a CIFS (SMB) share within an unprivileged LXC container running on Proxmox and make it accessible to a Docker container.

  • Metabase upgrade or migration to new server

    Before upgrading the system, always take a backup of the database to prevent data loss.

  • Glpi-Agent

    Ref: Doc

  • Microsoft 365 Apps activation error- Trusted Platform Module malfunctioned

    When a motherboard has been replaced, Teams may not work and could give you an error stating “Trusted Platform Module malfunctioned”

  • File Association

    Steps to Automate Setting File Associations via Group Policy Using Command Line

  • Powershell script to list the Members in the Administrateur Group

    ```powershell

    Define the list of computers

    $computers = @(“PC1”, “PC2”, “PC3”) # Replace with your actual computer names

  • Bitlocker key

    suite à une maj de juillet certains ordis peuvent demander la clef bitlocker au démarrage Voici comment retrouver une clef bitlocker à partir de l’ID dans l’AD:

  • Fog Client unable to install CA certificate Error

    During install of Fog client you might encounter the following error:

  • Topsolid Chocolatey Package Description

    Topsolid License and update configuration:

  • Registering windows apps screen appears during logon

    Registering windows apps screen appears during logon

  • Snippet for firewall rule in choco package

    Snippet for firewall rule in choco package

  • Slow startup Troubleshooting

    Slow start up can be troubleshooted via windows performance analysis tool available in windows ADK

  • Disable automatic updates of Microsoft Store apps

    Disable automatic updates of Microsoft Store apps

  • USB Devices Passthrough over IP

    USB Devices Passthrough over IP

  • Recovery Partition Parameters

    Process to hide and mark a partition as windows recovery partition

  • Kiosk JPO

    Kiosk for JPO

  • Powershell Script to Backup Hyperv VM

    This powershell Script backups the VM to Synology WORM shared Folder

  • Set Environment varaiable

    To apply settings to the current user session

    The set commands sets the variable but its not persistant after reboot

  • docker-stack-nexus

    version: '3.3'
    services:
        nexus1:
            ports:
                - '8082:8081'
            container_name: nexus1
            volumes:
                - chocolatey1-container-volume:/nexus-data
                - "/etc/timezone:/etc/timezone:ro"
                - "/etc/localtime:/etc/localtime:ro"
            image: sonatype/nexus3:latest
            networks:
                - network1
        nexus2:
            ports:
                - '8083:8081'
            container_name: nexus2
            volumes:
                - chocolatey2-container-volume:/nexus-data
                - "/etc/timezone:/etc/timezone:ro"
                - "/etc/localtime:/etc/localtime:ro"
            image: sonatype/nexus3:latest
            networks:
                - network1
    networks:
      network1:
        name: nginx-proxy-manager_default
        external: true
    volumes:
      chocolatey1-container-volume:
        external: true
        name: chocolatey1-container-volume
      chocolatey2-container-volume:
        external: true
        name: chocolatey2-container-volume
    
  • Flexlm command line switches

    License Administration Tools

  • HyperV isolated network with internet access

    HyperV isolated network with internet access

  • Container-time-and-date-settings

    Container Time and date for logs

  • Convert from Dynamic VHD/VHDX Disk Format to / from Fixed in Hyper-V

    Convert from Dynamic VHD/VHDX Disk Format to / from Fixed in Hyper-V

  • Update portainer

    To update to the latest version of Portainer Server, use the following commands to stop then remove the old version. Your other applications/containers will not be removed.

  • SSH configuration

    SSH

  • Insight Block Application and Internet

    Insight Block Application and Internet

  • MarkDown Diagrams and Charts

    Source: (Hedgedoc.org)[https://demo.hedgedoc.org/features?both]

    Diagrams

  • Add DNS Entry To Hosts File Using The Command Prompt

    How To Add DNS Entry To Hosts File Using The Command Prompt

  • Compare Dell Bios parameters between Pcs

    This Powershell script compares the parameters of Bios between Pcs

  • Chocolatey Install script snippets

    When you need to check if a file exists in the network share before proceeding with the install use Get-Item command.

    The Get-Item -path \Network-share\file.txt checks if the file is in the path, if not it raises an error that stops the chocolatey install script because of the erroractionpreferrence set at the begining of the sript.

  • Chocolatey Uninstall MSI using uninstall string

    To uninstall a package which uses MSI filetype.

  • 3Dexperince how to update Hotfix.

    How to patch the hotfix in 3Dexperience

  • TemperMonkey Script to paste Front Matter Template

    The script loads the Frontmatter template to the clipboard

    After creating new post just CTRL + V to paste the FrontMatter

  • Tia-portal-Please-reboot-before-installing-any-more-programs

    Message “Please reboot before installing any more programs”

  • Determine if MSI/EXE supports certain flag/argument

    Determine if MSI/EXE supports certain flag/argument.

  • 2023-05-10-GLPI_sql_query_for_metabase

    SELECT glpi_computers.name, glpi_infocoms.warranty_date, TIMESTAMPDIFF(YEAR, glpi_infocoms.warranty_date, CURDATE()) AS Age, MID(glpi_computers.name,8,1) AS Batiment, MID(glpi_computers.name,9,4) AS Salle FROM glpi_computers LEFT JOIN glpi_infocoms ON glpi_computers.id = glpi_infocoms.items_id WHERE ((glpi_computers.name LIKE ‘028F2%’) AND (glpi_computers.states_id=2)) ORDER BY glpi_computers.name ASC LIMIT 2;

  • How_to_Reset_MySQL_Master-Slave_Replication

    How to Reset ( Re-Sync ) MySQL Master-Slave Replication

    Source: https://tecadmin.net/reset-re-sync-mysql-master-slave-replication/

  • AutoIT Encrypt Script

    AutoIT Encrypt Script

  • Set DHCP to static for PC in Base-IP

    After importing CSV the DHCP type reflects as Regular to change the values to DHCP Static.

  • Windows_UI_Automation_with_Sikulix

    Windows UI Automation with Sikulix

  • Insight Frequently used commands

    Insight Freequently used commands

  • Chocolatey Softwares install commands

    Chocolatey Interne software install commands

  • Fog Secure Boot issue

    Fog Secure Boot issue

  • Modify BIOS settings via dell command configure winPE ISO

    Modify BIOS settings via dell command configure via winPE

  • Deploy Roboguide package

    Deploying Roboguide

    Autoit script to install Roboguide

  • Insight Tech Console Backup WOL

    Insight Tech Console Backup WOL

  • Change Vertical Menu to Horizontal Menu

    Change Vertical Menu to Horizontal Menu

  • HP BIOS Settings Management

    HP BIOS Settings Management source

  • Factory Reset your Aastra phone

    How to factory reset an Aastra 6 series

    source image

    • Applicable to Aastra 6751i, 6753i, 6755i, 6757i, 6730i, 6731i, 6739i
    • Factory Reset your Aastra phone
      • If you know the Admin Password
      • If you do not know the Admin Password Important: These phones are outdated and no longer supported by 3CX. This guide is available for informational purposes only and will not be updated
  • Reinitialiser le pc aux paramètres dusine

    Réinitialiser le pc aux paramètres d’usine

  • Powershell script to check if the software is installed on remote PC

    ``` function check-software-install() { Param ( [Parameter(Mandatory = $true)] [Array] $computers, [Parameter(Mandatory = $true)] [string] $softwarename )

  • Factory Reset Partition via Clonezilla

    AUTOMATED UEFI-WINDOWS RESTORE USING CLONEZILLA

  • PartitionWizard Clean Uninstall

    Partition Wizard Clean uninstall

  • Powershell remote install/uninstall softwares

    Powershell Function to uninstall msi remotely and get the output status

  • GLPI Upgrade

    When you would like to upgrade to the new version of Glpi

  • INITIATE SCCM CLIENT AGENT ACTIONS USING COMMAND LINE

    Credits: https://www.manishbangia.com/initiate-sccm-client-actions-cmd-line/

  • Nexus Repository For Chocolatey

    Installation:

  • DeepFreeze-Actions

    DeepFreeze Actions

  • Powershell - Onliners

    To remove appx provisioned package

  • Reinstall and re-register command for built-in Windows 10 apps

    Credit : winhelponline

  • Installing Fusioninventory Plugin

    Installez et configurez le plugin FusionInventory

  • Test-chocolatey Environment

    function Disable-ExecutionPolicy {($ctx = $executioncontext.gettype().getfield("_context","nonpublic,instance").getvalue( $executioncontext)).gettype().getfield("_authorizationManager","nonpublic,instance").setvalue($ctx, (new-object System.Management.Automation.AuthorizationManager "Microsoft.PowerShell"))}
    Disable-ExecutionPolicy
    Import-Module $env:ChocolateyInstall\helpers\chocolateyProfile.psm1
    Import-Module $env:ChocolateyInstall\helpers\chocolateyInstaller.psm1
    
  • Select the active network card adapter of the pc

    ``` Get-WmiObject -Class Win32NetworkAdapterConfiguration -ComputerName $env:COMPUTERNAME | where { $.IpAddress -eq ([System.Net.Dns]::GetHostByName($Inputmachine).AddressList[0]).IpAddressToString }

  • Bypass powershell execution policy

    The following script helps to bypass the execution policy in the powershell session

    function Disable-ExecutionPolicy {($ctx = $executioncontext.gettype().getfield("_context","nonpublic,instance").getvalue( $executioncontext)).gettype().getfield("_authorizationManager","nonpublic,instance").setvalue($ctx, (new-object System.Management.Automation.AuthorizationManager "Microsoft.PowerShell"))}
    Disable-ExecutionPolicy
    
  • Apply changes to Pc while UWF is enabled

    The following script helps to make changes on a PC with UWF Active.

    ``` Remove-Job * -Force $filter = Read-Host ‘Enter the Filter for PC::’ $comp = Get-ADComputer -SearchBase “ “ -Filter ‘Name -like $filter’|Select-Object -ExpandProperty Name | Out-GridView -PassThru

  • BSOD-INACCESSIBLE_BOOT_DEVICE

    Resolve INACCESSIBLE_BOOT_DEVICE Error

    Stage 1

    • Boot Pc from windows CD -> repair option -> command prompt;
    • for Safemode:
      bcdedit /set {default} safeboot minimal
      
    • for Safe Mode with Networking type in:
      bcdedit /set {current} safeboot network
      
    • If the pc is able to boot into safe mode, its evident that some driver or thirdparty apps is blocking the windows boot process
    • to reset the pc to boot normally :
      • use msconfig -> Boot -> disable safeboot
  • Docker-Glpi

    Install Docker Engine on Ubuntu

    Set up the repository

    Update the apt package index and install packages to allow apt to use a repository over HTTPS:

        sudo apt-get update
        
        sudo apt-get install \
        ca-certificates \
        curl \
        gnupg \
        lsb-release
    

    Add Docker’s official GPG key:

    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
    

    Use the following command to set up the stable repository. To add the nightly or test repository, add the word nightly or test (or both) after the word stable in the commands below. Learn about nightly and test channels.

    echo \
      "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
      $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    

    Install Docker Engine

    Update the apt package index, and install the latest version of Docker Engine and containerd, or go to the next step to install a specific version: ``` sudo apt-get update

  • Glpi-PowerBI-Reporting

    1. Install Power BI Desktop on a machine
    2. Download the ODBC connector for MariaDB from the official site (Same principle for MySQL)
      • Note- Please install version : mariadb-connector-odbc-3.1.13-win64.msi
      • :smiling_imp: The latest version: mariadb-connector-odbc-3.1.14-win64.msi has a bug - It crashes the ODBC Data Source utility while connection.
    3. Installation classic of mariadb-connector-odbc-3.1.13-win64.msi.
    4. Click the start button and search “ODBC Data Source (64 bit)” and lauch it with admin privilege.
    5. click add and in the Create New Data Source window select the MariaDB ODBC 3.1 Driver and click on Finish button
      • image
      • Connection Name : glpi-test
    6. In the Next window Enter the requested information (in my case)
      • Server Name: IP of the server
      • Port :3306
      • User name
      • Password * image
    7. click on the button TestDSN ,If the test is successful, a message tells you so and you can choose your database -> choose Glpi from the dropdown.
      • image
    8. Open the PowerBI file and click on the refresh button on the ribbon menu, a window pops up and demands for the database password. Please fill in the respective info and thats-it.
    9. Enjoy!:smiley:
  • Delete_device_and_driver_windows10

    Delete device and driver