From 04b0d12150bec8820f28bdeea16ea5e5fe3f5857 Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Thu, 25 Jun 2026 21:49:05 -0700 Subject: [PATCH] sync: auto-sync from HOWARD-HOME at 2026-06-25 21:48:38 Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-25 21:48:38 --- .claude/skills/drive-map/SKILL.md | 120 ++++++++++++ .claude/skills/drive-map/scripts/drive-map.sh | 181 ++++++++++++++++++ .../2026-06-25-howard-home-to-pro-upgrades.md | 106 ++++++++++ 3 files changed, 407 insertions(+) create mode 100644 .claude/skills/drive-map/SKILL.md create mode 100644 .claude/skills/drive-map/scripts/drive-map.sh create mode 100644 clients/cascades-tucson/session-logs/2026-06/2026-06-25-howard-home-to-pro-upgrades.md diff --git a/.claude/skills/drive-map/SKILL.md b/.claude/skills/drive-map/SKILL.md new file mode 100644 index 00000000..036557a0 --- /dev/null +++ b/.claude/skills/drive-map/SKILL.md @@ -0,0 +1,120 @@ +--- +name: drive-map +description: Reliably create/repoint Windows network drive maps and share shortcuts on a remote endpoint via GuruRMM. Bakes in the things that make this fight every time — runs in the user session (not SYSTEM, so maps actually appear), stores the per-host credential with cmdkey (the workgroup-PC-to-domain-share case), makes maps persistent, repoints/removes stale NAS shortcuts, and verifies access. Built for the Cascades NAS -> CS-SERVER migration but generic. +--- + +# drive-map — remote drive maps & share shortcuts that actually stick + +`net use` from RMM "doesn't work" for predictable reasons, and we kept re-solving +them by hand. This skill encodes the fixes so a repoint is one command. + +## The four things that make mapped drives fight you (and the fix this skill bakes in) + +1. **SYSTEM context is invisible to the user.** RMM runs as SYSTEM by default. A + drive mapped/shortcut written as SYSTEM lands in SYSTEM's profile, NOT the + logged-on user's — so the user sees nothing and you think it "failed." + **Fix:** every operation here runs `context: user_session`. It uses the user's + token, `[Environment]::GetFolderPath('Desktop')`, and the user's credential + vault. **Requires an active (logged-on) desktop session** — locked is usually + OK, logged-off is not. If no session, the skill reports it instead of silently + no-opping. +2. **Workgroup PC -> domain share = Access Denied** unless a credential is stored. + A machine that is not domain-joined (e.g. `DESKTOP-LPOPV30`, WORKGROUP, local + login) has no Kerberos/NTLM identity the server trusts. It must present a + **stored** credential for that exact server host. + **Fix:** `cred`/`migrate` run `cmdkey /add: /user: /pass:…` + in the user session so the credential lands in the user's Credential Manager, + keyed to the server host used in the UNC. (This is precisely how Karen reaches + the NAS today: `cmdkey` target `CASCADESDS`, user `karen rossini`.) +3. **Maps vanish on logoff.** `net use` without persistence is gone next login. + **Fix:** persistent by default (`/persistent:yes`); cmdkey credential is + persistent too, so the map reconnects without a prompt. +4. **Stale NAS shortcuts/maps linger.** The old `\\cascadesds\…` shortcut and the + live connection confuse users mid-migration. + **Fix:** `--remove-old ` repoints or deletes desktop shortcuts that + target the old prefix, drops the old drive letter, and removes the old cmdkey. + +## Credential handling (read this) + +- The password is read from the **SOPS vault** (`--cred-vault` + `--cred-field`), + never passed as plaintext on the command line (CLAUDE.md rule). +- **Caveat — it transits RMM.** cmdkey needs the plaintext on the endpoint, so the + password appears in the dispatched command text, which RMM stores in command + history (admin-only, internal). For a sensitive account, purge that history + entry afterward or rotate. The skill never prints the password to stdout/errorlog. +- For a domain account whose password we don't have, the correct move on a DC we + control is to set it deliberately and vault it first — do that, then run `cred`. + +## Usage + +``` +bash .claude/skills/drive-map/scripts/drive-map.sh --host [opts] +``` + +| Verb | Does | +|------|------| +| `verify` | Test-Path the target UNC/letter from the user session; report reachable or not. Read-only. | +| `cred` | Store a per-host credential (`cmdkey /add`) so the user can reach a server. | +| `map` | Map a drive letter to `\\HOST\Share` (persistent), optionally storing the cred first. | +| `shortcut` | Drop a desktop `.lnk` to a UNC target (optionally pin to Quick Access). | +| `unmap` | Remove a drive letter and/or desktop shortcuts pointing at `--remove-old`, and the old cmdkey. | +| `migrate` | The all-in-one repoint: remove/repoint old shortcut, store new cred, map and/or shortcut the new target, verify. | + +### Options + +``` +--host NAME RMM hostname (required; resolved to agent id, must be Windows + online) +--server '\\HOST\Share' target share for a drive map +--target '\\HOST\Share\Sub' UNC for a shortcut / verify +--letter X drive letter for map/unmap (no colon) +--name NAME shortcut filename (default: leaf of --target) +--cred-user 'DOMAIN\user' identity to store (e.g. CASCADES\karen.rossini) +--cred-vault PATH sops path holding the password (e.g. clients/cascades-tucson/...sops.yaml) +--cred-field FIELD field within the vault entry (default: credentials.password) +--remove-old '\\oldhost\share' prefix of stale shortcuts/connections to strip (migrate/unmap) +--quick-access also pin --target to Quick Access (best-effort; Shell verb) +--no-persistent non-persistent map (default is persistent) +--profile-hint NAME substring to disambiguate the user when several are logged on +--dry-run print the generated PowerShell, do not dispatch +--timeout N dispatch timeout seconds (default 90) +``` + +### Examples + +```bash +# Karen: workgroup PC, repoint NAS ALDocs shortcut to CS-SERVER, store her domain cred +bash .claude/skills/drive-map/scripts/drive-map.sh migrate \ + --host DESKTOP-LPOPV30 \ + --target '\\CS-SERVER\Server\ALDocs' --name ALDocs --quick-access \ + --remove-old '\\cascadesds\Server' \ + --cred-user 'CASCADES\karen.rossini' \ + --cred-vault clients/cascades-tucson/karen-rossini.sops.yaml + +# Just check a user can reach the new share +bash .claude/skills/drive-map/scripts/drive-map.sh verify --host DESKTOP-LPOPV30 \ + --target '\\CS-SERVER\Server\ALDocs' + +# Map a letter for a domain-joined user (no cred needed) +bash .claude/skills/drive-map/scripts/drive-map.sh map --host SOME-PC \ + --server '\\CS-SERVER\SalesDept' --letter S +``` + +## Hard rules + +- **Always `verify` first and last.** Confirm the target is reachable for that user + before declaring success — a green `net use` line is not proof of access. +- **One user at a time, with a session.** If no interactive user is logged on, stop + and say so; do not "succeed" against SYSTEM's profile. +- **Additive to permissions.** This skill never touches share/NTFS ACLs. If the user + lacks rights on the target, fix that on the server side (group membership), not here. +- **Confirm before mutating a live user's desktop** during business hours unless told + to proceed — it is outward-facing (the user sees their desktop change). +- On any genuine failure the script calls `log-skill-error.sh` (per CLAUDE.md). + +## Implementation + +- `scripts/drive-map.sh` — bash orchestrator: RMM auth (`rmm-auth.sh`), agent + resolution, vault read for the password, generates the endpoint PowerShell, + dispatches it `context: user_session`, polls, reports. All endpoint logic runs + in the user session by design (see fix #1). +- No endpoint install; the PowerShell is generated per call and dispatched via RMM. diff --git a/.claude/skills/drive-map/scripts/drive-map.sh b/.claude/skills/drive-map/scripts/drive-map.sh new file mode 100644 index 00000000..78fe2738 --- /dev/null +++ b/.claude/skills/drive-map/scripts/drive-map.sh @@ -0,0 +1,181 @@ +#!/usr/bin/env bash +# drive-map.sh — create/repoint Windows network drive maps + share shortcuts on a +# remote endpoint via GuruRMM, run in the user's session so they actually appear. +# See SKILL.md for the why. Verbs: verify | cred | map | shortcut | unmap | migrate. +set -uo pipefail +REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)" +VAULT="$REPO/.claude/scripts/vault.sh" +logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "drive-map" "$1" --context "${2:-}" >/dev/null 2>&1 || true; } +die(){ echo "[ERROR] $1" >&2; exit "${2:-2}"; } + +VERB="${1:-}"; shift || true +[ -n "$VERB" ] || die "usage: drive-map.sh --host [opts]" + +HOST=""; SERVER=""; TARGET=""; LETTER=""; NAME=""; CRED_USER=""; CRED_VAULT="" +CRED_FIELD="credentials.password"; REMOVE_OLD=""; QUICK=0; PERSIST="yes" +PROFILE_HINT=""; DRYRUN=0; TIMEOUT=90 +while [ $# -gt 0 ]; do + case "$1" in + --host) HOST="${2:?}"; shift 2;; + --server) SERVER="${2:?}"; shift 2;; + --target) TARGET="${2:?}"; shift 2;; + --letter) LETTER="${2:?}"; LETTER="${LETTER%%:}"; shift 2;; + --name) NAME="${2:?}"; shift 2;; + --cred-user) CRED_USER="${2:?}"; shift 2;; + --cred-vault) CRED_VAULT="${2:?}"; shift 2;; + --cred-field) CRED_FIELD="${2:?}"; shift 2;; + --remove-old) REMOVE_OLD="${2:?}"; shift 2;; + --quick-access) QUICK=1; shift;; + --no-persistent) PERSIST="no"; shift;; + --profile-hint) PROFILE_HINT="${2:?}"; shift 2;; + --dry-run) DRYRUN=1; shift;; + --timeout) TIMEOUT="${2:?}"; shift 2;; + *) die "unknown option: $1";; + esac +done +[ -n "$HOST" ] || die "--host is required" + +# Normalize a UNC: strip any/all leading backslashes, force exactly two. Shell-arg +# layers (Git-bash on Windows) collapse a leading "\\" to "\", which breaks the host +# parse; this makes the tool robust to 0/1/2 leading backslashes on input. +norm_unc(){ [ -n "$1" ] || return 0; printf '\\\\%s' "$(printf '%s' "$1" | sed -E 's#^\\+##')"; } +# host portion of a UNC: \\HOST\share\... -> HOST +unc_host(){ printf '%s' "$1" | sed -E 's#^\\+##; s#\\.*##'; } +# leaf of a UNC path: \\h\share\a\b -> b +unc_leaf(){ printf '%s' "$1" | sed 's#\\*$##; s#.*\\##'; } +SERVER="$(norm_unc "$SERVER")"; TARGET="$(norm_unc "$TARGET")"; REMOVE_OLD="$(norm_unc "$REMOVE_OLD")" + +# ---- resolve the credential password (vault only; never plaintext on CLI) ---- +CRED_PASS="" +if [ -n "$CRED_USER" ]; then + [ -n "$CRED_VAULT" ] || die "--cred-user given but no --cred-vault to read the password from" + CRED_PASS="$(bash "$VAULT" get-field "$CRED_VAULT" "$CRED_FIELD" 2>/dev/null)" + [ -n "$CRED_PASS" ] || { logerr "vault read empty for $CRED_VAULT:$CRED_FIELD" "host=$HOST"; die "could not read password at vault:$CRED_VAULT field:$CRED_FIELD"; } +fi + +# ---- generate the endpoint PowerShell for the verb (all user_session) ---- +# PS variable values are injected via single-quoted PS literals; we escape any +# embedded single quote by doubling it (PS rule). Password is masked in all output. +psq(){ printf "'%s'" "$(printf '%s' "$1" | sed "s/'/''/g")"; } + +build_ps(){ + local ps="" + # append one PS line with a REAL newline. Never use printf %b here: UNC paths + # contain backslashes (\c, \S) that %b would eat as escapes and truncate output. + add(){ ps+="$1"$'\n'; } + add "\$ErrorActionPreference='Continue'" + add "\$who = (Get-CimInstance Win32_ComputerSystem).UserName; if(-not \$who){ Write-Host '[WARN] no interactive user resolved; user-session ops may no-op' }" + add "Write-Host (\"[INFO] running as: \$env:USERNAME active: \$who\")" + + # remove-old: strip stale shortcuts/letter/cred pointing at the old prefix + if [ -n "$REMOVE_OLD" ] && { [ "$VERB" = "migrate" ] || [ "$VERB" = "unmap" ]; }; then + local oldhost; oldhost="$(unc_host "$REMOVE_OLD")" + add "\$old=$(psq "$REMOVE_OLD")" + add "\$desk=[Environment]::GetFolderPath('Desktop')" + add "\$wsh=New-Object -ComObject WScript.Shell" + add "Get-ChildItem \$desk -Filter *.lnk -EA SilentlyContinue | ForEach-Object { \$t=\$wsh.CreateShortcut(\$_.FullName).TargetPath; if(\$t -like (\$old+'*')){ Remove-Item \$_.FullName -Force -EA SilentlyContinue; Write-Host (\"[OK] removed stale shortcut: \"+\$_.Name+\" -> \"+\$t) } }" + add "cmdkey /delete:$oldhost *>\$null; Write-Host '[OK] cleared old cmdkey: $oldhost'" + [ -n "$LETTER" ] && add "net use ${LETTER}: /delete /y *>\$null" + fi + + # cred: store per-host credential + if [ -n "$CRED_USER" ] && { [ "$VERB" = "cred" ] || [ "$VERB" = "map" ] || [ "$VERB" = "migrate" ]; }; then + local credhost="" + [ -n "$SERVER" ] && credhost="$(unc_host "$SERVER")" + [ -z "$credhost" ] && [ -n "$TARGET" ] && credhost="$(unc_host "$TARGET")" + [ -n "$credhost" ] || die "cred needs --server or --target to derive the server host" + add "cmdkey /add:$credhost /user:$(psq "$CRED_USER") /pass:$(psq "$CRED_PASS") | Out-Null" + add "Write-Host '[OK] stored credential for $credhost as $CRED_USER'" + fi + + # map: persistent drive letter + if [ "$VERB" = "map" ]; then + [ -n "$SERVER" ] || die "map needs --server" + [ -n "$LETTER" ] || die "map needs --letter" + add "net use ${LETTER}: /delete /y *>\$null" + add "\$r = net use ${LETTER}: $(psq "$SERVER") /persistent:$PERSIST 2>&1; Write-Host (\"\$r\")" + add "if(Test-Path ${LETTER}:\\){ Write-Host '[OK] mapped ${LETTER}: -> $SERVER' } else { Write-Host '[FAIL] map ${LETTER}: failed'; exit 1 }" + fi + + # migrate may also map a letter if both server+letter given + if [ "$VERB" = "migrate" ] && [ -n "$SERVER" ] && [ -n "$LETTER" ]; then + add "net use ${LETTER}: /delete /y *>\$null" + add "\$r = net use ${LETTER}: $(psq "$SERVER") /persistent:$PERSIST 2>&1; Write-Host (\"\$r\")" + add "if(Test-Path ${LETTER}:\\){ Write-Host '[OK] mapped ${LETTER}: -> $SERVER' } else { Write-Host '[WARN] map ${LETTER}: did not verify' }" + fi + + # shortcut: desktop .lnk (+ optional quick access) + if [ -n "$TARGET" ] && { [ "$VERB" = "shortcut" ] || [ "$VERB" = "migrate" ]; }; then + local lname; lname="${NAME:-$(unc_leaf "$TARGET")}" + add "\$desk2=[Environment]::GetFolderPath('Desktop')" + add "\$wsh2=New-Object -ComObject WScript.Shell" + add "\$lp=Join-Path \$desk2 ($(psq "$lname")+'.lnk')" + add "\$lnk=\$wsh2.CreateShortcut(\$lp); \$lnk.TargetPath=$(psq "$TARGET"); \$lnk.Save()" + add "Write-Host ('[OK] shortcut: '+\$lp+' -> $TARGET')" + if [ "$QUICK" = "1" ]; then + add "try { \$sa=New-Object -ComObject Shell.Application; \$sa.Namespace($(psq "$TARGET")).Self.InvokeVerb('pintohome'); Write-Host '[OK] pinned to Quick Access' } catch { Write-Host '[WARN] Quick Access pin failed (non-fatal)' }" + fi + fi + + # verify: target reachable from this user + if [ -n "$TARGET" ] && { [ "$VERB" = "verify" ] || [ "$VERB" = "migrate" ]; }; then + add "if(Test-Path -LiteralPath $(psq "$TARGET")){ Write-Host '[OK] reachable: $TARGET' } else { Write-Host '[FAIL] NOT reachable: $TARGET'; exit 1 }" + elif [ "$VERB" = "verify" ] && [ -n "$LETTER" ]; then + add "if(Test-Path ${LETTER}:\\){ Write-Host '[OK] reachable: ${LETTER}:' } else { Write-Host '[FAIL] NOT reachable: ${LETTER}:'; exit 1 }" + fi + printf '%s' "$ps" +} + +PS_SCRIPT="$(build_ps)" +[ -n "$PS_SCRIPT" ] || die "verb '$VERB' produced no actions — check options" + +if [ "$DRYRUN" = "1" ]; then + echo "[DRY-RUN] would dispatch to $HOST (user_session), timeout ${TIMEOUT}s:" + echo "------------------------------------------------------------" + # mask the password in the preview + if [ -n "$CRED_PASS" ]; then printf '%s\n' "$PS_SCRIPT" | sed "s/$(printf '%s' "$CRED_PASS" | sed 's/[.[\*^$()+?{|]/\\&/g')/********/g"; else printf '%s\n' "$PS_SCRIPT"; fi + echo "------------------------------------------------------------" + exit 0 +fi + +# ---- RMM auth + agent resolution ---- +eval "$(bash "$REPO/.claude/scripts/rmm-auth.sh")" >/dev/null 2>&1 || { logerr "rmm auth failed"; die "RMM auth failed"; } +AGENTS="$(curl -s "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")" +AGENT="$(echo "$AGENTS" | jq --arg h "$HOST" '[.[] | select(.hostname|ascii_downcase|contains($h|ascii_downcase))] | .[0]')" +AID="$(echo "$AGENT" | jq -r '.id // empty')" +AHOST="$(echo "$AGENT" | jq -r '.hostname // empty')" +AOS="$(echo "$AGENT" | jq -r '.os_type // empty')" +ACONN="$(echo "$AGENT" | jq -r '.is_connected // false')" +[ -n "$AID" ] || { logerr "agent not found: $HOST"; die "no RMM agent matching '$HOST'"; } +[ "$AOS" = "windows" ] || die "agent $AHOST is os_type=$AOS — drive-map is Windows-only" +echo "[INFO] target: $AHOST (id=$AID, connected=$ACONN) verb=$VERB" +[ "$ACONN" = "true" ] || echo "[WARN] agent offline — command will queue until it reconnects" + +PAYLOAD="$(jq -n --arg ct powershell --arg cmd "$PS_SCRIPT" --argjson to "$TIMEOUT" \ + '{command_type:$ct, command:$cmd, timeout_seconds:$to, context:"user_session"}')" +CMD_ID="$(curl -s -X POST "$RMM/api/agents/$AID/command" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$PAYLOAD" | jq -r '.command_id // empty')" +[ -n "$CMD_ID" ] || { logerr "dispatch failed" "host=$AHOST verb=$VERB"; die "dispatch failed"; } +echo "[INFO] dispatched cmd=$CMD_ID (user_session)" + +S=""; R="" +for i in $(seq 1 30); do + R="$(curl -s "$RMM/api/commands/$CMD_ID" -H "Authorization: Bearer $TOKEN")" + S="$(echo "$R" | jq -r '.status')" + { [ "$S" = "completed" ] || [ "$S" = "failed" ] || [ "$S" = "cancelled" ] || [ "$S" = "interrupted" ]; } && break + sleep 4 +done +echo "[INFO] status=$S exit=$(echo "$R" | jq -r '.exit_code // "—"')" +echo "--- stdout ---"; echo "$R" | jq -r '.stdout // ""' +STDERR="$(echo "$R" | jq -r '.stderr // ""')" +[ -n "$STDERR" ] && { echo "--- stderr ---"; echo "$STDERR"; } + +# alert per RMM convention (write op only) +if [ "$VERB" != "verify" ]; then + bash "$REPO/.claude/scripts/post-bot-alert.sh" "[RMM] drive-map $VERB on $AHOST -> ${TARGET:-$SERVER} (status=$S)" >/dev/null 2>&1 || true +fi + +case "$S" in + completed) exit 0;; + failed) logerr "drive-map $VERB failed on $AHOST" "cmd=$CMD_ID"; exit 1;; + *) logerr "drive-map $VERB ended status=$S on $AHOST" "cmd=$CMD_ID"; exit 1;; +esac diff --git a/clients/cascades-tucson/session-logs/2026-06/2026-06-25-howard-home-to-pro-upgrades.md b/clients/cascades-tucson/session-logs/2026-06/2026-06-25-howard-home-to-pro-upgrades.md new file mode 100644 index 00000000..5f216e7b --- /dev/null +++ b/clients/cascades-tucson/session-logs/2026-06/2026-06-25-howard-home-to-pro-upgrades.md @@ -0,0 +1,106 @@ +## User +- **User:** Howard Enos (howard) +- **Machine:** Howard-Home +- **Role:** tech + +## Session Summary + +Resumed the paused Cascades of Tucson Windows Home -> Pro upgrade workstream (prereq for +domain-joining the remaining staff PCs; Windows Home cannot domain-join). Started by loading +context from the wiki, `REMAINING-WORK-PLAN.md`, and the `feedback_windows_pro_upgrade_billing` +memory, then ran a live RMM status check on the 5 target Home machines. Of the five +(LAPTOP-8P7HDSEI, MDIRECTOR-PC, MEMRECEPT-PC, NurseAssist, SALES4-PC), three were online, +NurseAssist was offline (and flagged as a possible duplicate of Assistnurse-pc), and SALES4-PC +was offline and bypassed per the decision to repurpose Tamra's departing machine. + +Per Howard's direction, ran the edition upgrade using the generic public Pro key first to verify +the process before consuming a MAK count. Confirmed all three online boxes were `EditionID=Core` +(Home) with no logged-on users, then dispatched `changepk.exe /productkey VK7JG-NPHTM-C97JM-9MPGT-3V66T` +via RMM. As SYSTEM, changepk flips the edition in-place without auto-rebooting, which left the +registry EditionID and the licensing service out of sync. A single reboot per machine reconciled +them: all three came back reading Professional. MDIRECTOR-PC self-activated as genuine Pro via a +built-in digital entitlement (no MAK, no charge). The other two needed activation. + +Applied the ACG MAK to MEMRECEPT-PC and LAPTOP-8P7HDSEI (`slmgr /ipk` + `/ato`). Discovered the +ACG MAK is actually a Windows Pro *for Workstations* MAK — `/ipk` retargets the edition to +ProfessionalWorkstation (a higher SKU, fine for domain join). LAPTOP-8P7HDSEI activated; MEMRECEPT-PC +hit a transient `0x8004FE92` on first `/ato` and activated on retry. Both Licensed via VOLUME_MAK. + +Created Syncro ticket #32466 under Cascades, added a customer-visible work note, created a reusable +taxable "Windows Pro Upgrade" product ($99), and invoiced: 2x $99 keys (MEMRECEPT-PC, LAPTOP-8P7HDSEI, +machine named per line) plus 1.0h remote labor. Cascades has a 47.75h prepaid block, so the labor +auto-deducted ($0 on the invoice, block -> 46.75); invoice total $215.23 with AZ tax on the keys. +MDIRECTOR-PC was not billed (free digital activation). + +## Key Decisions + +- Generic Pro key first, MAK second — verify the edition flip works before consuming/billing a MAK + count (Howard's explicit instruction). +- Did the upgrades remotely via RMM rather than waiting for the onsite batch, since no users were + logged in (~8:45 PM Tucson) so the reboots were non-disruptive. +- Rebooted to reconcile the half-applied edition state (changepk as SYSTEM does not auto-reboot; + registry vs licensing diverge until a reboot). +- MDIRECTOR-PC self-activated free via digital entitlement -> NOT billed (billing rule keys $99 to + MAK usage, which was not consumed for that machine). +- Created a dedicated taxable "Windows Pro Upgrade" Syncro product (id 23571919, $99) for clean + QuickBooks mapping and reuse on future Home->Pro billing; taxable to match ACG's other license + products (Howard confirmed). +- 1.0h remote labor drawn from the Cascades prepaid block (standard behavior for a block customer). + +## Problems Encountered + +- First changepk dispatch failed before running: doubled single-quotes around a PowerShell registry + path inside a single-quoted bash `$SCRIPT` collapsed the string, leaving the "Windows NT" path + unquoted; `$ErrorActionPreference="Stop"` aborted on line 1, so changepk never executed (machines + untouched). Fixed by using double-quotes for paths inside the single-quoted bash script. Logged to + errorlog as --friction (ref feedback_windows_quote_stripping). +- MEMRECEPT-PC `/ato` transient `0x8004FE92` on first attempt -> succeeded on retry (ancient Pentium + box on a 100 Mbps NIC; likely a momentary reach to the activation server). +- LAPTOP-8P7HDSEI briefly reported `EditionID=Enterprise` mid-transition; self-resolved to Professional + after the reboot. + +## Configuration Changes + +- Modified: `clients/cascades-tucson/docs/REMAINING-WORK-PLAN.md` — recorded the 3 boxes upgraded to + Pro (process, results, billing) and that NurseAssist/SALES4-PC remain pending. +- Modified: `.claude/memory/feedback_windows_pro_upgrade_billing.md` — MAK is Pro-for-Workstations; + the working remote flow; per-machine activation status; invoiced status (#32466). +- Appended: `errorlog.md` — bash/PowerShell quoting friction entry. +- Created (in Syncro, not repo): Windows Pro Upgrade product, ticket #32466, invoice #1650806091. + +## Credentials & Secrets + +- ACG Windows Pro (for Workstations) MAK: vault `infrastructure/windows-pro-mak.sops.yaml`, field + `credentials.product_key`. Used `/ipk` + `/ato` on MEMRECEPT-PC and LAPTOP-8P7HDSEI (2 counts + consumed). No new secrets created. Generic public Pro key (not secret): VK7JG-NPHTM-C97JM-9MPGT-3V66T. + +## Infrastructure & Servers + +- GuruRMM: http://172.16.3.30:3001. Cascades agent IDs — MDIRECTOR-PC 6b7990aa-edad-41c7-8f2d-5efdcaa41046, + MEMRECEPT-PC e93ac0b6-c593-4fa2-b1f7-56ebf3816efb, LAPTOP-8P7HDSEI d8e9502f-7061-4574-8cc3-a84f60bb3471, + NurseAssist fc88f14b-06eb-47ac-b9e6-971c44d700ba (offline), SALES4-PC 975f70d8-cd6d-45d7-9da1-6ce2f1ae59ab (offline). +- Syncro: Cascades of Tucson customer_id 20149445; prepay block 47.75 -> 46.75 hrs. + +## Commands & Outputs + +- Edition flip (per machine, as SYSTEM): `changepk.exe /productkey VK7JG-NPHTM-C97JM-9MPGT-3V66T` + -> EditionID Core->Professional, exitcode 0, no auto-reboot. Reboot once to sync registry+licensing. +- Activation: `cscript //nologo slmgr.vbs /ipk ` then `/ato`; retry `/ato` if `0x8004FE92`. + Verify with `/dli` (License Status: Licensed, VOLUME_MAK). +- Trust `EditionID` over `ProductName` (ProductName reads "Windows 10 Home" on Win11 boxes - stale). + +## Pending / Incomplete Tasks + +- Domain-join the 3 now-Pro boxes (MDIRECTOR-PC, MEMRECEPT-PC, LAPTOP-8P7HDSEI) into cascades.local + -> dept OUs -> drives. Deferred to tomorrow (Howard). +- NurseAssist: offline; verify it is a real distinct machine vs a duplicate of Assistnurse-pc before + upgrading/joining. +- SALES4-PC: bypassed (Tamra departing); decide repurpose vs upgrade. +- Broader Cascades migration workstreams continue per REMAINING-WORK-PLAN.md. + +## Reference Information + +- Syncro ticket #32466 (id 113090740): https://computerguru.syncromsp.com/tickets/113090740 +- Invoice #1650806091 — total $215.23 (2x $99 keys + AZ tax; 1.0h labor applied to block). +- Syncro product "Windows Pro Upgrade" id 23571919 ($99, taxable). +- Generic Pro key: VK7JG-NPHTM-C97JM-9MPGT-3V66T (edition flip only, does not activate).