Reorganize repo: compartmentalize scripts by client/project

Move 150+ scripts from root and scripts/ into client/project directories:
- clients/dataforth/scripts/ (110 files: AD2, sync, SSH, DB, DOS scripts)
- clients/bg-builders/scripts/ (14 files: Lesley mgmt, Exchange, termination)
- clients/internal-infrastructure/scripts/ (10 files: GDAP, Gitea, backups)
- projects/msp-tools/scripts/ (9 files: CIPP, MSP onboarding, Datto)
- projects/gururmm-agent/scripts/ (3 files: API test, JWT, record counts)
- clients/glaztech/scripts/ (1 file: CentraStage removal)

Also reorganized:
- VPN scripts → infrastructure/vpn-configs/
- Retrieved API/JS files → api/
- Forum posts → projects/community-forum/forum-posts/
- SSH docs → clients/internal-infrastructure/docs/
- NWTOC/CTONW docs → projects/wrightstown-smarthome/docs/
- ACG website files → projects/internal/acg-website-2025/
- Dataforth docs → clients/dataforth/docs/
- schema-retrieved.sql → docs/database/

Deleted 24 tmp_*.ps1 one-off debug scripts (preserved in git history).
Root reduced from 220+ files to 62 items (docs + directories only).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 17:15:07 -07:00
parent 98ea867d2c
commit 5cbd49ce24
207 changed files with 49 additions and 547 deletions

View File

@@ -0,0 +1,138 @@
# SSH Passwordless Access Setup
**Problem:** Automated deployments require password entry, causing delays and requiring manual intervention.
**Solution:** One-time SSH key setup enables fully automated deployments forever.
---
## Quick Setup (One Command)
Run this PowerShell command **once** with your RMM password:
```powershell
cd D:\ClaudeTools
.\setup-ssh-keys.ps1
```
When prompted for password, enter your RMM password. You'll enter it **3 times total** (for pscp, mkdir, and key install).
**After this ONE-TIME setup:**
- `deploy.ps1` will work without ANY prompts
- `pscp` commands work automatically
- `plink` commands work automatically
- No more 4-hour debugging sessions due to deployment issues
---
## What It Does
1. **Generates SSH key pair** (already done: `~/.ssh/id_rsa`)
2. **Copies public key** to RMM server
3. **Configures authorized_keys** for guru user
4. **Tests passwordless access**
Total time: 30 seconds
---
## Alternative: Manual Setup
If you prefer to do it manually:
```bash
# 1. Copy public key to RMM server
pscp %USERPROFILE%\.ssh\id_rsa.pub guru@172.16.3.30:/tmp/claude_key.pub
# 2. SSH to RMM and install key
plink guru@172.16.3.30
mkdir -p ~/.ssh
chmod 700 ~/.ssh
cat /tmp/claude_key.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
rm /tmp/claude_key.pub
exit
# 3. Test passwordless access
plink -batch guru@172.16.3.30 "echo 'Success!'"
```
---
## Verification
After setup, this command should work WITHOUT password prompt:
```powershell
plink -batch guru@172.16.3.30 "echo 'Passwordless SSH working!'"
```
**Expected output:** `Passwordless SSH working!`
**If it prompts for password:** Setup failed, re-run `setup-ssh-keys.ps1`
---
## Why This Matters
**Before SSH keys:**
- Every `deploy.ps1` run requires 3-5 password entries
- Cannot run automated deployments
- Manual file copying required
- High risk of deploying wrong files
- 4+ hours wasted debugging version mismatches
**After SSH keys:**
- `.\deploy.ps1` - ONE command, ZERO prompts
- Fully automated version checking
- Automatic file deployment
- Service restart without intervention
- Post-deployment verification
- **Total deployment time: 30 seconds**
---
## Security Notes
**SSH Key Location:** `C:\Users\MikeSwanson\.ssh\id_rsa` (private key)
**Public Key Location:** `C:\Users\MikeSwanson\.ssh\id_rsa.pub`
**Key Type:** RSA 4096-bit
**Passphrase:** None (enables automation)
**Access:** Only your Windows user account can read the private key
**RMM Access:** Only guru@172.16.3.30 can use this key
**Note:** The private key file has restricted permissions. Keep it secure.
---
## Troubleshooting
**"FATAL ERROR: Cannot answer interactive prompts in batch mode"**
- SSH keys not installed yet
- Run `setup-ssh-keys.ps1` to install them
**"Permission denied (publickey,password)"**
- authorized_keys file has wrong permissions
- On RMM: `chmod 600 ~/.ssh/authorized_keys`
**"Could not resolve hostname"**
- Network issue
- Verify RMM server is reachable: `ping 172.16.3.30`
---
## Next Steps
1. **Run setup script:** `.\setup-ssh-keys.ps1`
2. **Verify it works:** `plink -batch guru@172.16.3.30 "whoami"`
3. **Deploy safeguards:** `.\deploy.ps1`
4. **Never waste 4 hours again**
---
**Status:** SSH key generated ✓
**Action Required:** Run `setup-ssh-keys.ps1` once to install on RMM server
**Time Required:** 30 seconds
**Password Entries:** 3 (one-time only)
**Future Password Entries:** 0 (automated forever)

View File

@@ -0,0 +1,418 @@
# SSH Connection Investigation Report
**Investigation Date:** 2026-01-17
**Agent:** SSH/Network Connection Agent
**Issue:** 5 lingering SSH processes + 1 ssh-agent process
---
## Executive Summary
**ROOT CAUSE IDENTIFIED:** Git operations in hooks are spawning SSH processes, but **NOT** for remote repository access. The SSH processes are related to:
1. **Git for Windows SSH configuration** (`core.sshcommand = C:/Windows/System32/OpenSSH/ssh.exe`)
2. **Credential helper operations** (credential.https://git.azcomputerguru.com.provider=generic)
3. **Background sync operations** launched by hooks (`sync-contexts &`)
**IMPORTANT:** The repository uses HTTPS, NOT SSH for git remote operations:
- Remote URL: `https://git.azcomputerguru.com/azcomputerguru/claudetools.git`
- Authentication: Generic credential provider (Windows Credential Manager)
---
## Investigation Findings
### 1. Git Commands in Hooks
**File:** `.claude/hooks/user-prompt-submit`
```bash
Line 42: git config --local claude.projectid
Line 46: git config --get remote.origin.url
```
**File:** `.claude/hooks/task-complete`
```bash
Line 40: git config --local claude.projectid
Line 43: git config --get remote.origin.url
Line 63: git rev-parse --abbrev-ref HEAD
Line 64: git rev-parse --short HEAD
Line 67: git diff --name-only HEAD~1
Line 75: git log -1 --pretty=format:"%s"
```
**Analysis:**
- These commands are **LOCAL ONLY** - they do NOT contact remote repository
- `git config --local` = local .git/config only
- `git config --get remote.origin.url` = reads from local config (no network)
- `git rev-parse` = local repository operations
- `git diff HEAD~1` = local diff (no network)
- `git log -1` = local log (no network)
**Conclusion:** Git commands in hooks should NOT spawn SSH processes for network operations.
---
### 2. Background Sync Operations
**File:** `.claude/hooks/user-prompt-submit` (Line 68)
```bash
bash "$(dirname "${BASH_SOURCE[0]}")/sync-contexts" >/dev/null 2>&1 &
```
**File:** `.claude/hooks/task-complete` (Lines 171, 178)
```bash
bash "$(dirname "${BASH_SOURCE[0]}")/sync-contexts" >/dev/null 2>&1 &
bash "$(dirname "${BASH_SOURCE[0]}")/sync-contexts" >/dev/null 2>&1 &
```
**Analysis:**
- Both hooks spawn `sync-contexts` in background (`&`)
- `sync-contexts` uses `curl` to POST to API (HTTP, not SSH)
- Each hook execution spawns a NEW background process
**Process Chain:**
```
Claude Code Hook
└─> bash user-prompt-submit
├─> git config (spawns: bash → git.exe → possibly ssh for credential helper)
└─> bash sync-contexts & (background)
└─> curl (HTTP to 172.16.3.30:8001)
```
**Zombie Accumulation:**
- `user-prompt-submit` runs BEFORE each user message
- `task-complete` runs AFTER task completion
- Both spawn background `sync-contexts` processes
- Background processes may not properly terminate
- Each git operation spawns: bash → git → OpenSSH (due to core.sshcommand)
---
### 3. Git Configuration Analysis
**Global Git Config:**
```
core.sshcommand = C:/Windows/System32/OpenSSH/ssh.exe
credential.https://git.azcomputerguru.com.provider = generic
```
**Why SSH processes spawn:**
1. **Git for Windows** is configured to use Windows OpenSSH (`C:/Windows/System32/OpenSSH/ssh.exe`)
2. Even though remote is HTTPS, git may invoke SSH for:
- Credential helper operations
- GPG signing (if configured)
- SSH agent for key management
3. **Credential provider** is set to `generic` for the gitea server
- This may use Windows Credential Manager
- Credential operations might trigger ssh-agent
**SSH-Agent Purpose:**
- SSH agent (`ssh-agent.exe`) manages SSH keys
- Even with HTTPS remote, git might use ssh-agent for:
- GPG commit signing with SSH keys
- Credential helper authentication
- Git LFS operations (if configured)
---
### 4. Process Lifecycle Issues
**Expected Lifecycle:**
```
Hook starts → git config → git spawns ssh → command completes → ssh terminates → hook ends
```
**Actual Behavior (suspected):**
```
Hook starts → git config → git spawns ssh → command completes → ssh lingers (orphaned)
→ sync-contexts & → spawns in background → may not terminate
→ curl to API
```
**Why processes linger:**
1. **Background processes (`&`)**:
- `sync-contexts` runs in background
- Parent hook terminates before child completes
- Background process becomes orphaned
- Bash shell keeps running to manage background job
2. **Git spawns SSH but doesn't wait for cleanup**:
- Git uses OpenSSH for credential operations
- SSH process may outlive git command
- No explicit process cleanup
3. **Windows process management**:
- Orphaned processes don't auto-terminate on Windows
- Need explicit cleanup or timeout
---
### 5. Hook Execution Frequency
**Trigger Points:**
- `user-prompt-submit`: Runs BEFORE every user message
- `task-complete`: Runs AFTER task completion (less frequent)
**Accumulation Pattern:**
```
Session Start: 0 SSH processes
User message 1: +1-2 SSH processes (user-prompt-submit)
User message 2: +1-2 SSH processes (accumulating)
User message 3: +1-2 SSH processes (now 3-6 total)
Task complete: +1-2 SSH processes (task-complete)
...
```
After 5-10 interactions: **5-10 zombie SSH processes**
---
## Root Cause Summary
**Primary Cause:** Background `sync-contexts` processes spawned by hooks
**Secondary Cause:** Git commands trigger OpenSSH for credential/signing operations
**Contributing Factors:**
1. Hooks spawn background processes with `&` (lines 68, 171, 178)
2. Background processes are not tracked or cleaned up
3. Git is configured with `core.sshcommand` pointing to OpenSSH
4. Each git operation potentially spawns ssh for credential helper
5. Windows doesn't auto-cleanup orphaned processes
6. No timeout or process cleanup mechanism in hooks
---
## Why Git Uses SSH (Despite HTTPS Remote)
Git may invoke SSH even with HTTPS remotes for:
1. **Credential Helper**: Generic credential provider might use ssh-agent
2. **GPG Signing**: If commits are signed with SSH keys (git 2.34+)
3. **Git Config**: `core.sshcommand` explicitly tells git to use OpenSSH
4. **Credential Storage**: Windows Credential Manager accessed via ssh-agent
5. **Git LFS**: Large File Storage might use SSH for authentication
**Evidence:**
```bash
git config --global core.sshcommand
# Output: C:/Windows/System32/OpenSSH/ssh.exe
git config --global credential.https://git.azcomputerguru.com.provider
# Output: generic
```
---
## Recommended Fixes
### Fix #1: Remove Background Process Spawning (HIGH PRIORITY)
**Problem:** Hooks spawn `sync-contexts` in background with `&`
**Solution:** Remove background spawning or add proper cleanup
**Files to modify:**
- `.claude/hooks/user-prompt-submit` (line 68)
- `.claude/hooks/task-complete` (lines 171, 178)
**Options:**
**Option A - Remove background spawn (synchronous):**
```bash
# Instead of:
bash "$(dirname "${BASH_SOURCE[0]}")/sync-contexts" >/dev/null 2>&1 &
# Use:
bash "$(dirname "${BASH_SOURCE[0]}")/sync-contexts" >/dev/null 2>&1
```
**Pros:** Simple, no zombies
**Cons:** Slower hook execution (blocks on sync)
**Option B - Remove sync from hooks entirely:**
```bash
# Comment out or remove the sync-contexts calls
# Let user manually run: bash .claude/hooks/sync-contexts
```
**Pros:** No blocking, no zombies
**Cons:** Requires manual sync or cron job
**Option C - Add timeout and cleanup:**
```bash
# Run with timeout and background cleanup
timeout 10s bash "$(dirname "${BASH_SOURCE[0]}")/sync-contexts" >/dev/null 2>&1 &
SYNC_PID=$!
# Register cleanup trap
trap "kill $SYNC_PID 2>/dev/null" EXIT
```
**Pros:** Non-blocking with cleanup
**Cons:** More complex, timeout command may not exist on Windows Git Bash
---
### Fix #2: Reduce Git Command Frequency (MEDIUM PRIORITY)
**Problem:** Every hook execution runs multiple git commands
**Solution:** Cache git values to reduce spawning
**Example optimization:**
```bash
# Cache project ID in environment variable or temp file
if [ -z "$CACHED_PROJECT_ID" ]; then
PROJECT_ID=$(git config --local claude.projectid 2>/dev/null)
export CACHED_PROJECT_ID="$PROJECT_ID"
else
PROJECT_ID="$CACHED_PROJECT_ID"
fi
```
**Impact:** 50% reduction in git command executions
---
### Fix #3: Review Git SSH Configuration (LOW PRIORITY)
**Problem:** Git uses SSH even for HTTPS operations
**Investigation needed:**
1. Why is `core.sshcommand` set to OpenSSH?
2. Is SSH needed for credential helper?
3. Is GPG signing using SSH keys?
**Potential fix:**
```bash
# Remove core.sshcommand if not needed
git config --global --unset core.sshcommand
# Or use Git Credential Manager instead of generic
git config --global credential.helper manager-core
```
**WARNING:** Test thoroughly before changing - may break authentication
---
### Fix #4: Add Process Cleanup to Hooks (MEDIUM PRIORITY)
**Problem:** No cleanup of spawned processes
**Solution:** Add trap handlers to kill child processes on exit
**Example:**
```bash
#!/bin/bash
# Add at top of hook
cleanup() {
# Kill all child processes
jobs -p | xargs kill 2>/dev/null
}
trap cleanup EXIT
# ... rest of hook ...
```
---
## Testing Plan
1. **Verify SSH processes before fix:**
```powershell
Get-Process | Where-Object {$_.Name -eq 'ssh' -or $_.Name -eq 'ssh-agent'}
```
2. **Apply Fix #1 (remove background spawn)**
3. **Test hook execution:**
- Send 5 user messages to Claude
- Check SSH process count after each message
4. **Verify SSH processes after fix:**
- Should remain constant (1 ssh-agent max)
- No accumulation of ssh.exe processes
5. **Monitor for 24 hours:**
- Check process count periodically
- Verify no zombie accumulation
---
## Questions Answered
**Q1: Why are git operations spawning SSH?**
A: Git is configured with `core.sshcommand = OpenSSH` and may use SSH for credential helper operations, even with HTTPS remote.
**Q2: Are hooks deliberately syncing with git remote?**
A: NO. Hooks sync to API (http://172.16.3.30:8001) via curl, not git remote.
**Q3: Is ssh-agent supposed to be running?**
A: YES - 1 ssh-agent is normal for Git operations. 5+ ssh.exe processes is NOT normal.
**Q4: Are SSH connections timing out or accumulating?**
A: ACCUMULATING. Background processes spawn ssh and don't properly terminate.
**Q5: Is ControlMaster/ControlPersist keeping connections alive?**
A: NO - no SSH config file found with ControlMaster settings.
**Q6: Are hooks SUPPOSED to sync with git remote?**
A: NO - this appears to be unintentional side effect of:
- Background process spawning
- Git credential helper using SSH
- No process cleanup
---
## File Mapping: Which Hooks Spawn SSH
| Hook File | Git Commands | Background Spawn | SSH Risk |
|-----------|-------------|------------------|----------|
| `user-prompt-submit` | 2 git commands | YES (line 68) | HIGH |
| `task-complete` | 5 git commands | YES (2x: lines 171, 178) | CRITICAL |
| `sync-contexts` | 0 git commands | N/A | NONE (curl only) |
| `periodic-context-save` | 1 git command | Unknown | MEDIUM |
**Highest risk:** `task-complete` (spawns background process TWICE + 5 git commands)
---
## Recommended Action Plan
**Immediate (Today):**
1. Apply Fix #1 Option B: Comment out background sync calls in hooks
2. Test with 10 user messages
3. Verify SSH process count remains stable
**Short-term (This Week):**
1. Implement manual sync command or scheduled task for `sync-contexts`
2. Add caching for git values to reduce command frequency
3. Add process cleanup traps to hooks
**Long-term (Future):**
1. Review git SSH configuration necessity
2. Consider alternative credential helper
3. Investigate if GPG/SSH signing is needed
4. Optimize hook execution performance
---
## Success Criteria
**Fix is successful when:**
- SSH process count remains constant (1 ssh-agent max)
- No accumulation of ssh.exe processes over time
- Hooks execute without spawning orphaned background processes
- Context sync still works (either manual or scheduled)
**Monitoring metrics:**
- SSH process count over 24 hours
- Hook execution time
- Context sync success rate
- User message latency
---
**Report Compiled By:** SSH/Network Connection Agent
**Status:** Investigation Complete - Root Cause Identified
**Next Step:** Apply Fix #1 and monitor

View File

@@ -0,0 +1,104 @@
# Reset Gitea password for mike@azcomputerguru.com via SSH
# Runs on Jupiter server (172.16.3.20)
$JupiterHost = "172.16.3.20"
$JupiterUser = "root"
$JupiterPassword = "Th1nk3r^99##"
Write-Host "=== Gitea Password Reset ===" -ForegroundColor Cyan
Write-Host ""
# Prompt for new password
$NewPassword = Read-Host "Enter new Gitea password" -AsSecureString
$ConfirmPassword = Read-Host "Confirm password" -AsSecureString
# Convert to plain text for comparison
$NewPasswordPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($NewPassword))
$ConfirmPasswordPlain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
[Runtime.InteropServices.Marshal]::SecureStringToBSTR($ConfirmPassword))
if ($NewPasswordPlain -ne $ConfirmPasswordPlain) {
Write-Host "[ERROR] Passwords do not match" -ForegroundColor Red
exit 1
}
if ([string]::IsNullOrWhiteSpace($NewPasswordPlain)) {
Write-Host "[ERROR] Password cannot be empty" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "[1] Connecting to Jupiter server..." -ForegroundColor Yellow
# Build SSH command to reset Gitea password in Docker container
$GiteaCommand = @"
# Find Gitea Docker container
echo '[1] Finding Gitea container...'
CONTAINER=`$(docker ps --filter 'name=gitea' --format '{{.Names}}' | head -n 1)
if [ -z "`$CONTAINER" ]; then
echo '[ERROR] Cannot find Gitea container'
echo 'Available containers:'
docker ps --format '{{.Names}}'
exit 1
fi
echo '[OK] Found container: '`$CONTAINER
echo ''
echo '[2] Resetting password for mike@azcomputerguru.com...'
# Execute gitea admin command inside container
# Try username 'mike' first, then email
docker exec `$CONTAINER gitea admin user change-password --username mike --password '$NewPasswordPlain' 2>&1 || \
docker exec `$CONTAINER gitea admin user change-password --username mike@azcomputerguru.com --password '$NewPasswordPlain' 2>&1
if [ `$? -eq 0 ]; then
echo ''
echo '[SUCCESS] Password changed successfully!'
exit 0
else
echo ''
echo '[ERROR] Failed to change password'
exit 1
fi
"@
# Execute via SSH using plink (or ssh if available)
try {
if (Get-Command plink -ErrorAction SilentlyContinue) {
# Use PuTTY's plink
$result = echo y | plink -ssh -batch -pw $JupiterPassword "$JupiterUser@$JupiterHost" $GiteaCommand 2>&1
} elseif (Get-Command ssh -ErrorAction SilentlyContinue) {
# Use OpenSSH
# Note: This will prompt for password interactively
Write-Host "[INFO] Using OpenSSH - you'll need to enter root password: $JupiterPassword" -ForegroundColor Yellow
$result = ssh "$JupiterUser@$JupiterHost" $GiteaCommand 2>&1
} else {
Write-Host "[ERROR] No SSH client found (plink or ssh)" -ForegroundColor Red
Write-Host ""
Write-Host "Manual steps:" -ForegroundColor Yellow
Write-Host "1. SSH to Jupiter: ssh root@172.16.3.20" -ForegroundColor Gray
Write-Host "2. Find container: docker ps | grep gitea" -ForegroundColor Gray
Write-Host "3. Reset password: docker exec <container_name> gitea admin user change-password --username mike --password 'YOUR_PASSWORD'" -ForegroundColor Gray
exit 1
}
Write-Host $result
Write-Host ""
Write-Host "=== Password Reset Complete ===" -ForegroundColor Cyan
Write-Host ""
Write-Host "Login at: https://git.azcomputerguru.com/" -ForegroundColor Green
Write-Host "Username: mike@azcomputerguru.com (or just 'mike')" -ForegroundColor Green
Write-Host "Password: (the one you just set)" -ForegroundColor Green
} catch {
Write-Host ""
Write-Host "[ERROR] Failed to connect: $($_.Exception.Message)" -ForegroundColor Red
Write-Host ""
Write-Host "Manual alternative:" -ForegroundColor Yellow
Write-Host "1. SSH to Jupiter: ssh root@172.16.3.20 (password: $JupiterPassword)" -ForegroundColor Gray
Write-Host "2. Find Gitea container: docker ps | grep gitea" -ForegroundColor Gray
Write-Host "3. Reset password: docker exec <container_name> gitea admin user change-password --username mike --password 'YOUR_PASSWORD'" -ForegroundColor Gray
}

View File

@@ -0,0 +1,81 @@
# Reset password for notifications@dataforth.com using Exchange Online
# This works when Microsoft Graph permissions are insufficient
Write-Host "[OK] Resetting password via Azure AD (using web portal method)..." -ForegroundColor Green
Write-Host ""
$UserPrincipalName = "notifications@dataforth.com"
# Generate a strong password
Add-Type -AssemblyName System.Web
$NewPassword = [System.Web.Security.Membership]::GeneratePassword(16, 4)
Write-Host "================================================================"
Write-Host "PASSWORD RESET OPTIONS"
Write-Host "================================================================"
Write-Host ""
Write-Host "[OPTION 1] Use Azure AD Portal (Recommended - Always Works)" -ForegroundColor Cyan
Write-Host ""
Write-Host "1. Open browser to: https://portal.azure.com"
Write-Host "2. Navigate to: Azure Active Directory > Users"
Write-Host "3. Search for: notifications@dataforth.com"
Write-Host "4. Click 'Reset password'"
Write-Host "5. Use this generated password: $NewPassword" -ForegroundColor Yellow
Write-Host "6. UNCHECK 'Make this user change password on first sign in'"
Write-Host ""
Write-Host "[OPTION 2] Use PowerShell with Elevated Admin Account" -ForegroundColor Cyan
Write-Host ""
Write-Host "If you have a Global Admin account, connect to Azure AD:"
Write-Host ""
Write-Host "Install-Module AzureAD -Scope CurrentUser" -ForegroundColor Gray
Write-Host "Connect-AzureAD -TenantId 7dfa3ce8-c496-4b51-ab8d-bd3dcd78b584" -ForegroundColor Gray
Write-Host "`$Password = ConvertTo-SecureString '$NewPassword' -AsPlainText -Force" -ForegroundColor Gray
Write-Host "Set-AzureADUserPassword -ObjectId notifications@dataforth.com -Password `$Password -ForceChangePasswordNextSignIn `$false" -ForegroundColor Gray
Write-Host ""
Write-Host "================================================================"
Write-Host "RECOMMENDED PASSWORD"
Write-Host "================================================================"
Write-Host ""
Write-Host " $NewPassword" -ForegroundColor Yellow
Write-Host ""
Write-Host "SAVE THIS PASSWORD for the website configuration!"
Write-Host ""
# Save to file
$CredPath = "D:\ClaudeTools\dataforth-notifications-NEW-PASSWORD.txt"
@"
Dataforth Notifications Account - PASSWORD RESET
Generated: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Username: notifications@dataforth.com
NEW Password: $NewPassword
IMPORTANT: Password policy is already set to never expire!
You just need to reset the actual password.
SMTP Configuration for Website:
- Server: smtp.office365.com
- Port: 587
- TLS: Yes
- Username: notifications@dataforth.com
- Password: $NewPassword
STATUS:
- Password Never Expires: YES (already configured)
- Password Reset: PENDING (use Azure portal or PowerShell above)
DO NOT COMMIT TO GIT OR SHARE PUBLICLY
"@ | Out-File -FilePath $CredPath -Encoding UTF8
Write-Host "[OK] Instructions and password saved to:" -ForegroundColor Green
Write-Host " $CredPath" -ForegroundColor Cyan
Write-Host ""
Write-Host "================================================================"
Write-Host "AFTER RESETTING PASSWORD"
Write-Host "================================================================"
Write-Host "1. Update website SMTP config with new password"
Write-Host "2. Test: D:\ClaudeTools\Test-DataforthSMTP.ps1"
Write-Host "3. Verify: Get-MessageTrace -SenderAddress notifications@dataforth.com"
Write-Host ""

View File

@@ -0,0 +1,165 @@
# Add Rob Williams and Howard to all GDAP Security Groups
# This fixes CIPP access issues for multiple users
$ErrorActionPreference = "Stop"
# Configuration
$TenantId = "ce61461e-81a0-4c84-bb4a-7b354a9a356d"
$ClientId = "fabb3421-8b34-484b-bc17-e46de9703418"
$ClientSecret = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO"
# Users to add to GDAP groups
$UsersToAdd = @(
"rob@azcomputerguru.com",
"howard@azcomputerguru.com"
)
# GDAP Groups (from analysis)
$GdapGroups = @(
@{Name="M365 GDAP Cloud App Security Administrator"; Id="009e46ef-3ffa-48fb-9568-7e8cb7652200"},
@{Name="M365 GDAP Application Administrator"; Id="16e99bf8-a0bc-41d3-adf7-ce89310cece5"},
@{Name="M365 GDAP Teams Administrator"; Id="35fafd80-498c-4c62-a947-ea230835d9f1"},
@{Name="M365 GDAP Security Administrator"; Id="3ca0d8b1-a6fc-4e77-a955-2a7d749d27b4"},
@{Name="M365 GDAP Privileged Role Administrator"; Id="49b1b90d-d7bf-4585-8fe2-f2a037f7a374"},
@{Name="M365 GDAP Cloud Device Administrator"; Id="8e866fc5-c4bd-4ce7-a273-385857a4f3b4"},
@{Name="M365 GDAP Exchange Administrator"; Id="92401e16-c217-4330-9bbd-6a978513452d"},
@{Name="M365 GDAP User Administrator"; Id="baf461df-c675-4f9e-a4a3-8f03c6fe533d"},
@{Name="M365 GDAP Privileged Authentication Administrator"; Id="c593633a-2957-4069-ae7e-f862a0896b67"},
@{Name="M365 GDAP Intune Administrator"; Id="daad8ec5-d044-4d4c-bae7-5df98a637c95"},
@{Name="M365 GDAP SharePoint Administrator"; Id="fa55c8c1-34e3-46b7-912e-f4d303081a82"},
@{Name="M365 GDAP Authentication Policy Administrator"; Id="fdf38f92-8dd1-470d-8ce8-58f663235789"},
@{Name="AdminAgents"; Id="ecc00632-9de6-4932-a62b-de57b72c1414"}
)
Write-Host "[INFO] Authenticating to Microsoft Graph..." -ForegroundColor Cyan
# Get access token
$TokenBody = @{
client_id = $ClientId
client_secret = $ClientSecret
scope = "https://graph.microsoft.com/.default"
grant_type = "client_credentials"
}
$TokenResponse = Invoke-RestMethod -Method Post `
-Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" `
-Body $TokenBody
$Headers = @{
Authorization = "Bearer $($TokenResponse.access_token)"
}
Write-Host "[OK] Authenticated successfully" -ForegroundColor Green
Write-Host ""
# Process each user
$TotalSuccessCount = 0
$TotalSkippedCount = 0
$TotalErrorCount = 0
foreach ($UserUpn in $UsersToAdd) {
Write-Host "="*80 -ForegroundColor Cyan
Write-Host "PROCESSING USER: $UserUpn" -ForegroundColor Cyan
Write-Host "="*80 -ForegroundColor Cyan
# Get user ID
Write-Host "[INFO] Looking up user..." -ForegroundColor Cyan
try {
$User = Invoke-RestMethod -Method Get `
-Uri "https://graph.microsoft.com/v1.0/users/$UserUpn" `
-Headers $Headers
Write-Host "[OK] Found user:" -ForegroundColor Green
Write-Host " Display Name: $($User.displayName)"
Write-Host " UPN: $($User.userPrincipalName)"
Write-Host " ID: $($User.id)"
Write-Host ""
$UserId = $User.id
}
catch {
Write-Host "[ERROR] User not found: $($_.Exception.Message)" -ForegroundColor Red
Write-Host ""
continue
}
# Add user to each group
$SuccessCount = 0
$SkippedCount = 0
$ErrorCount = 0
foreach ($Group in $GdapGroups) {
Write-Host "[INFO] Adding to: $($Group.Name)" -ForegroundColor Cyan
# Check if already a member
try {
$Members = Invoke-RestMethod -Method Get `
-Uri "https://graph.microsoft.com/v1.0/groups/$($Group.Id)/members" `
-Headers $Headers
$IsMember = $Members.value | Where-Object { $_.id -eq $UserId }
if ($IsMember) {
Write-Host "[SKIP] Already a member" -ForegroundColor Yellow
$SkippedCount++
continue
}
}
catch {
Write-Host "[WARNING] Could not check membership: $($_.Exception.Message)" -ForegroundColor Yellow
}
# Add to group
try {
$Body = @{
"@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$UserId"
} | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri "https://graph.microsoft.com/v1.0/groups/$($Group.Id)/members/`$ref" `
-Headers $Headers `
-Body $Body `
-ContentType "application/json" | Out-Null
Write-Host "[SUCCESS] Added to group" -ForegroundColor Green
$SuccessCount++
}
catch {
Write-Host "[ERROR] Failed to add: $($_.Exception.Message)" -ForegroundColor Red
$ErrorCount++
}
Start-Sleep -Milliseconds 500 # Rate limiting
}
# User summary
Write-Host ""
Write-Host "Summary for $($User.displayName):" -ForegroundColor Cyan
Write-Host " Successfully added: $SuccessCount groups" -ForegroundColor Green
Write-Host " Already member of: $SkippedCount groups" -ForegroundColor Yellow
Write-Host " Errors: $ErrorCount groups" -ForegroundColor $(if($ErrorCount -gt 0){"Red"}else{"Green"})
Write-Host ""
$TotalSuccessCount += $SuccessCount
$TotalSkippedCount += $SkippedCount
$TotalErrorCount += $ErrorCount
}
Write-Host ""
Write-Host "="*80 -ForegroundColor Cyan
Write-Host "FINAL SUMMARY" -ForegroundColor Cyan
Write-Host "="*80 -ForegroundColor Cyan
Write-Host "Total users processed: $($UsersToAdd.Count)"
Write-Host "Total additions: $TotalSuccessCount groups" -ForegroundColor Green
Write-Host "Total already members: $TotalSkippedCount groups" -ForegroundColor Yellow
Write-Host "Total errors: $TotalErrorCount groups" -ForegroundColor $(if($TotalErrorCount -gt 0){"Red"}else{"Green"})
Write-Host ""
if ($TotalSuccessCount -gt 0 -or $TotalSkippedCount -gt 0) {
Write-Host "[OK] Users should now be able to access all client tenants through CIPP!" -ForegroundColor Green
Write-Host "[INFO] It may take 5-10 minutes for group membership to fully propagate." -ForegroundColor Cyan
Write-Host "[INFO] Ask users to sign out of CIPP and sign back in." -ForegroundColor Cyan
}
else {
Write-Host "[WARNING] Some operations failed. Review errors above." -ForegroundColor Yellow
}

View File

@@ -0,0 +1,47 @@
# Check if Node.js server is running on AD2
Write-Host "[OK] Checking Node.js server status..." -ForegroundColor Green
# Test 1: Check port 3000
Write-Host "`n[TEST 1] Testing port 3000..." -ForegroundColor Cyan
$portTest = Test-NetConnection -ComputerName 192.168.0.6 -Port 3000 -WarningAction SilentlyContinue -InformationLevel Quiet
if ($portTest) {
Write-Host " [OK] Port 3000 is OPEN" -ForegroundColor Green
} else {
Write-Host " [ERROR] Port 3000 is CLOSED" -ForegroundColor Red
Write-Host " [INFO] Node.js server is not running" -ForegroundColor Yellow
}
# Test 2: Check Node.js processes (via SMB)
Write-Host "`n[TEST 2] Checking for Node.js processes..." -ForegroundColor Cyan
$password = ConvertTo-SecureString 'Paper123!@#' -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential('INTRANET\sysadmin', $password)
try {
$nodeProcs = Invoke-Command -ComputerName 192.168.0.6 -Credential $cred -ScriptBlock {
Get-Process node -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, @{Name='Memory(MB)';Expression={[math]::Round($_.WorkingSet64/1MB,2)}}, Path
} -ErrorAction Stop
if ($nodeProcs) {
Write-Host " [OK] Node.js process(es) found:" -ForegroundColor Green
$nodeProcs | Format-Table -AutoSize
} else {
Write-Host " [ERROR] No Node.js process found" -ForegroundColor Red
}
} catch {
Write-Host " [WARNING] WinRM check failed: $($_.Exception.Message)" -ForegroundColor Yellow
Write-Host " [INFO] Cannot verify via WinRM, but port test is more reliable" -ForegroundColor Cyan
}
Write-Host "`n[SUMMARY]" -ForegroundColor Cyan
if (!$portTest) {
Write-Host " Node.js server is NOT running" -ForegroundColor Red
Write-Host "`n [ACTION] Start the server on AD2:" -ForegroundColor Yellow
Write-Host " cd C:\Shares\testdatadb" -ForegroundColor Cyan
Write-Host " node server.js" -ForegroundColor Cyan
} else {
Write-Host " Node.js server appears to be running" -ForegroundColor Green
Write-Host " But API endpoints are not responding - check server logs" -ForegroundColor Yellow
}
Write-Host "`n[OK] Done" -ForegroundColor Green

View File

@@ -0,0 +1,37 @@
$password = ConvertTo-SecureString 'Paper123!@#' -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential('INTRANET\sysadmin', $password)
Invoke-Command -ComputerName 192.168.0.6 -Credential $cred -ScriptBlock {
Write-Host "=== Files in _COMMON\ProdSW ===" -ForegroundColor Yellow
Get-ChildItem "C:\Shares\test\_COMMON\ProdSW" -File |
Select-Object Name, Length, LastWriteTime |
Sort-Object Name |
Format-Table -AutoSize
Write-Host ""
Write-Host "=== Files in COMMON\ProdSW ===" -ForegroundColor Yellow
Get-ChildItem "C:\Shares\test\COMMON\ProdSW" -File |
Select-Object Name, Length, LastWriteTime |
Sort-Object Name |
Format-Table -AutoSize
Write-Host ""
Write-Host "=== Files ONLY in _COMMON\ProdSW ===" -ForegroundColor Cyan
$underFiles = Get-ChildItem "C:\Shares\test\_COMMON\ProdSW" -File | Select-Object -ExpandProperty Name
$commonFiles = Get-ChildItem "C:\Shares\test\COMMON\ProdSW" -File | Select-Object -ExpandProperty Name
$onlyInUnder = $underFiles | Where-Object { $_ -notin $commonFiles }
if ($onlyInUnder) {
$onlyInUnder | ForEach-Object { Write-Host " $_" -ForegroundColor White }
} else {
Write-Host " (none)" -ForegroundColor Gray
}
Write-Host ""
Write-Host "=== Files ONLY in COMMON\ProdSW ===" -ForegroundColor Cyan
$onlyInCommon = $commonFiles | Where-Object { $_ -notin $underFiles }
if ($onlyInCommon) {
$onlyInCommon | ForEach-Object { Write-Host " $_" -ForegroundColor White }
} else {
Write-Host " (none)" -ForegroundColor Gray
}
}

View File

@@ -0,0 +1,80 @@
$password = ConvertTo-SecureString 'Paper123!@#' -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential('INTRANET\sysadmin', $password)
Write-Host "Creating Sync Task via XML..." -ForegroundColor Cyan
Invoke-Command -ComputerName 192.168.0.6 -Credential $cred -ScriptBlock {
# Create task XML
$taskXml = @'
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Description>Sync test data and software updates between NAS and AD2</Description>
</RegistrationInfo>
<Triggers>
<TimeTrigger>
<Repetition>
<Interval>PT15M</Interval>
<StopAtDurationEnd>false</StopAtDurationEnd>
</Repetition>
<StartBoundary>2026-01-20T10:00:00</StartBoundary>
<Enabled>true</Enabled>
</TimeTrigger>
</Triggers>
<Principals>
<Principal id="Author">
<UserId>INTRANET\sysadmin</UserId>
<LogonType>Password</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<ExecutionTimeLimit>PT30M</ExecutionTimeLimit>
</Settings>
<Actions Context="Author">
<Exec>
<Command>powershell.exe</Command>
<Arguments>-ExecutionPolicy Bypass -NonInteractive -File "C:\Shares\test\scripts\Sync-FromNAS.ps1"</Arguments>
<WorkingDirectory>C:\Shares\test\scripts</WorkingDirectory>
</Exec>
</Actions>
</Task>
'@
# Save XML to file
$xmlPath = "C:\Temp\sync-task.xml"
$taskXml | Out-File -FilePath $xmlPath -Encoding Unicode -Force
Write-Host "[1] Deleting old task if exists..." -ForegroundColor Yellow
schtasks /Delete /TN "Sync-FromNAS" /F 2>$null
Write-Host "[2] Creating task from XML..." -ForegroundColor Yellow
$result = schtasks /Create /XML $xmlPath /TN "Sync-FromNAS" /RU "INTRANET\sysadmin" /RP "Paper123!@#" /F
Write-Host " $result" -ForegroundColor White
Write-Host ""
Write-Host "[3] Starting task..." -ForegroundColor Yellow
schtasks /Run /TN "Sync-FromNAS"
Write-Host ""
Write-Host " Waiting 30 seconds for sync..." -ForegroundColor White
Start-Sleep -Seconds 30
Write-Host ""
Write-Host "[4] Checking task status..." -ForegroundColor Yellow
schtasks /Query /TN "Sync-FromNAS" /FO LIST /V
Write-Host ""
Write-Host "[5] Last 25 lines of sync log..." -ForegroundColor Yellow
if (Test-Path "C:\Shares\test\scripts\sync-from-nas.log") {
Get-Content "C:\Shares\test\scripts\sync-from-nas.log" | Select-Object -Last 25 | ForEach-Object { Write-Host " $_" -ForegroundColor Gray }
}
}

View File

@@ -0,0 +1,26 @@
# Find machine folders with Backup subdirectories
$testPath = "\\192.168.0.6\test"
Write-Host "Searching for machine folders with Backup subdirectories in $testPath"
Write-Host ""
$machineFolders = Get-ChildItem -Path $testPath -Directory -ErrorAction SilentlyContinue
foreach ($folder in $machineFolders) {
$backupPath = Join-Path $folder.FullName "Backup"
if (Test-Path $backupPath) {
Write-Host "FOUND: $($folder.Name) -> $backupPath"
# Check if there are any files in the backup folder
$backupFiles = Get-ChildItem -Path $backupPath -File -ErrorAction SilentlyContinue
if ($backupFiles) {
Write-Host " Files: $($backupFiles.Count) files found"
Write-Host " Latest: $($backupFiles | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { "$($_.Name) - $($_.LastWriteTime)" })"
} else {
Write-Host " (Empty backup folder)"
}
Write-Host ""
}
}
Write-Host "Search complete."

View File

@@ -0,0 +1,47 @@
# Find TS-XX machine folders with Backup subdirectories
$testPath = "\\192.168.0.6\test"
Write-Host "Searching for TS-XX machine folders with Backup subdirectories..."
Write-Host ""
# Get all directories matching TS-*
$tsFolders = Get-ChildItem -Path $testPath -Directory -Filter "TS-*" -ErrorAction SilentlyContinue
if ($tsFolders) {
Write-Host "Found $($tsFolders.Count) TS-XX folders:"
Write-Host ""
foreach ($folder in $tsFolders) {
$backupPath = Join-Path $folder.FullName "Backup"
Write-Host "Machine: $($folder.Name)"
if (Test-Path $backupPath) {
Write-Host " [OK] Backup folder exists: $backupPath"
# Check for files in backup folder
$backupFiles = Get-ChildItem -Path $backupPath -ErrorAction SilentlyContinue
if ($backupFiles) {
Write-Host " Files: $($backupFiles.Count) items"
# Show most recent file
$latestFile = $backupFiles | Where-Object { -not $_.PSIsContainer } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($latestFile) {
Write-Host " Latest: $($latestFile.Name)"
Write-Host " Date: $($latestFile.LastWriteTime)"
Write-Host " Size: $([math]::Round($latestFile.Length / 1KB, 2)) KB"
}
} else {
Write-Host " [EMPTY] No files in backup folder"
}
} else {
Write-Host " [MISSING] No Backup folder"
}
Write-Host ""
}
} else {
Write-Host "No TS-XX folders found in $testPath"
}
Write-Host "Search complete."

View File

@@ -0,0 +1,72 @@
#!/bin/bash
# Reset Gitea password for mike@azcomputerguru.com
# Run this on Jupiter server (172.16.3.20) as root
# Note: Gitea runs in Docker container
echo "=== Gitea Password Reset (Docker) ==="
echo ""
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo "[ERROR] This script must be run as root"
exit 1
fi
# Find Gitea Docker container
echo "[1] Finding Gitea Docker container..."
CONTAINER=$(docker ps --filter 'name=gitea' --format '{{.Names}}' | head -n 1)
if [ -z "$CONTAINER" ]; then
echo "[ERROR] Cannot find Gitea container"
echo ""
echo "Available containers:"
docker ps --format '{{.Names}}'
exit 1
fi
echo "[OK] Found container: $CONTAINER"
echo ""
echo "[2] Current Gitea users:"
docker exec $CONTAINER gitea admin user list 2>/dev/null || \
echo "[WARNING] Could not list users"
echo ""
echo "[3] Resetting password for: mike@azcomputerguru.com"
echo ""
# Prompt for new password
read -sp "Enter new password: " NEW_PASSWORD
echo ""
read -sp "Confirm password: " CONFIRM_PASSWORD
echo ""
if [ "$NEW_PASSWORD" != "$CONFIRM_PASSWORD" ]; then
echo "[ERROR] Passwords do not match"
exit 1
fi
if [ -z "$NEW_PASSWORD" ]; then
echo "[ERROR] Password cannot be empty"
exit 1
fi
echo ""
echo "[4] Executing password change..."
# Reset password using docker exec (try username 'mike' first, then email)
docker exec $CONTAINER gitea admin user change-password --username mike --password "$NEW_PASSWORD" 2>&1 || \
docker exec $CONTAINER gitea admin user change-password --username mike@azcomputerguru.com --password "$NEW_PASSWORD" 2>&1
if [ $? -eq 0 ]; then
echo ""
echo "[SUCCESS] Password reset complete!"
echo ""
echo "You can now login at: https://git.azcomputerguru.com/"
echo "Username: mike@azcomputerguru.com (or just 'mike')"
echo "Password: (the one you just set)"
else
echo ""
echo "[ERROR] Password reset failed"
echo "Try running manually with different username format"
fi

View File

@@ -0,0 +1,47 @@
# Test the API endpoint directly
Write-Host "[OK] Testing API endpoints on AD2..." -ForegroundColor Green
# Test 1: Stats endpoint
Write-Host "`n[TEST 1] GET /api/stats" -ForegroundColor Cyan
try {
$response = Invoke-WebRequest -Uri "http://192.168.0.6:3000/api/stats" -Method GET -UseBasicParsing -ErrorAction Stop
Write-Host "[OK] Status: $($response.StatusCode)" -ForegroundColor Green
Write-Host "[OK] Response:" -ForegroundColor Green
$response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 5
} catch {
Write-Host "[ERROR] Request failed: $($_.Exception.Message)" -ForegroundColor Red
Write-Host "[ERROR] Response: $($_.Exception.Response)" -ForegroundColor Red
if ($_.ErrorDetails.Message) {
Write-Host "[ERROR] Details: $($_.ErrorDetails.Message)" -ForegroundColor Red
}
}
# Test 2: Search endpoint (simple)
Write-Host "`n[TEST 2] GET /api/search?limit=1" -ForegroundColor Cyan
try {
$response = Invoke-WebRequest -Uri "http://192.168.0.6:3000/api/search?limit=1" -Method GET -UseBasicParsing -ErrorAction Stop
Write-Host "[OK] Status: $($response.StatusCode)" -ForegroundColor Green
Write-Host "[OK] Response:" -ForegroundColor Green
$response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 5
} catch {
Write-Host "[ERROR] Request failed: $($_.Exception.Message)" -ForegroundColor Red
if ($_.ErrorDetails.Message) {
Write-Host "[ERROR] Details: $($_.ErrorDetails.Message)" -ForegroundColor Red
}
}
# Test 3: Record by ID
Write-Host "`n[TEST 3] GET /api/record/1" -ForegroundColor Cyan
try {
$response = Invoke-WebRequest -Uri "http://192.168.0.6:3000/api/record/1" -Method GET -UseBasicParsing -ErrorAction Stop
Write-Host "[OK] Status: $($response.StatusCode)" -ForegroundColor Green
Write-Host "[OK] Response:" -ForegroundColor Green
$response.Content | ConvertFrom-Json | ConvertTo-Json -Depth 5
} catch {
Write-Host "[ERROR] Request failed: $($_.Exception.Message)" -ForegroundColor Red
if ($_.ErrorDetails.Message) {
Write-Host "[ERROR] Details: $($_.ErrorDetails.Message)" -ForegroundColor Red
}
}
Write-Host "`n[OK] Done" -ForegroundColor Green