Synced files: - Quote wizard frontend (all components, hooks, types, config) - API updates (config, models, routers, schemas, services) - Client work (bg-builders, gurushow) - Scripts (BGB Lesley termination, CIPP, Datto, migration) - Temp files (Bardach contacts, VWP investigation, misc) - Credentials and session logs - Email service, PHP API, session logs Machine: ACG-M-L5090 Timestamp: 2026-03-10 19:11:00 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
218 lines
7.5 KiB
Python
218 lines
7.5 KiB
Python
"""Analyze Temp vs Contacts folders for merge strategy."""
|
|
import subprocess, json, sys
|
|
|
|
TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f"
|
|
CLAUDE_APP = "fabb3421-8b34-484b-bc17-e46de9703418"
|
|
CLAUDE_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO"
|
|
USER = "barbara@bardach.net"
|
|
|
|
# All contact fields we want to preserve
|
|
SELECT_FIELDS = (
|
|
"id,displayName,givenName,surname,middleName,nickName,"
|
|
"emailAddresses,homePhones,businessPhones,"
|
|
"companyName,jobTitle,department,"
|
|
"homeAddress,businessAddress,otherAddress,"
|
|
"birthday,personalNotes,"
|
|
"categories,title,generation,imAddresses,"
|
|
"parentFolderId"
|
|
)
|
|
|
|
def get_token():
|
|
r = subprocess.run([
|
|
'curl', '-s', '-X', 'POST',
|
|
f'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token',
|
|
'-d', f'client_id={CLAUDE_APP}&client_secret={CLAUDE_SECRET}&scope=https://graph.microsoft.com/.default&grant_type=client_credentials'
|
|
], capture_output=True, text=True)
|
|
return json.loads(r.stdout)['access_token']
|
|
|
|
def pull_contacts(token, folder_id=None, folder_name="default"):
|
|
if folder_id:
|
|
base = f"https://graph.microsoft.com/v1.0/users/{USER}/contactFolders/{folder_id}/contacts"
|
|
else:
|
|
base = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts"
|
|
|
|
url = f"{base}?$top=100&$select={SELECT_FIELDS}"
|
|
all_contacts = []
|
|
page = 0
|
|
|
|
while url:
|
|
page += 1
|
|
r = subprocess.run(['curl', '-s', '-H', f'Authorization: Bearer {token}', url],
|
|
capture_output=True, text=True)
|
|
data = json.loads(r.stdout)
|
|
|
|
if 'error' in data:
|
|
print(f" Error: {data['error'].get('message','')[:200]}")
|
|
break
|
|
|
|
items = data.get('value', [])
|
|
all_contacts.extend(items)
|
|
url = data.get('@odata.nextLink')
|
|
|
|
if page % 10 == 0:
|
|
print(f" {folder_name}: page {page}, {len(all_contacts)} contacts...")
|
|
|
|
if not items:
|
|
break
|
|
|
|
print(f" {folder_name}: {len(all_contacts)} total")
|
|
return all_contacts
|
|
|
|
token = get_token()
|
|
print("[OK] Token acquired\n")
|
|
|
|
# Get folder IDs
|
|
print("Getting contact folders...")
|
|
r = subprocess.run(['curl', '-s', '-H', f'Authorization: Bearer {token}',
|
|
f'https://graph.microsoft.com/v1.0/users/{USER}/contactFolders?$select=displayName,id'],
|
|
capture_output=True, text=True)
|
|
folders = json.loads(r.stdout).get('value', [])
|
|
temp_id = None
|
|
contacts_id = None
|
|
for f in folders:
|
|
print(f" {f['displayName']}: {f['id'][:40]}...")
|
|
if f['displayName'] == 'Temp':
|
|
temp_id = f['id']
|
|
elif f['displayName'] == 'Contacts':
|
|
contacts_id = f['id']
|
|
|
|
if not temp_id:
|
|
print("[ERROR] Temp folder not found!")
|
|
sys.exit(1)
|
|
|
|
# Pull both folders
|
|
print("\nPulling Contacts folder...")
|
|
main_contacts = pull_contacts(token, contacts_id, "Contacts")
|
|
|
|
print("\nPulling Temp folder...")
|
|
temp_contacts = pull_contacts(token, temp_id, "Temp")
|
|
|
|
# Save raw data
|
|
with open('D:/ClaudeTools/temp/bardach_main_contacts.json', 'w') as f:
|
|
json.dump(main_contacts, f, indent=2)
|
|
with open('D:/ClaudeTools/temp/bardach_temp_contacts.json', 'w') as f:
|
|
json.dump(temp_contacts, f, indent=2)
|
|
print(f"\nSaved raw data files")
|
|
|
|
# Build matching keys
|
|
def make_key(c):
|
|
"""Create a matching key from name."""
|
|
name = (c.get('displayName') or '').strip().lower()
|
|
return name
|
|
|
|
def make_email_keys(c):
|
|
"""Get all email addresses as keys."""
|
|
return set(e.get('address', '').strip().lower()
|
|
for e in c.get('emailAddresses', [])
|
|
if e.get('address'))
|
|
|
|
# Index main contacts
|
|
main_by_name = {}
|
|
main_by_email = {}
|
|
for c in main_contacts:
|
|
key = make_key(c)
|
|
if key:
|
|
main_by_name.setdefault(key, []).append(c)
|
|
for email in make_email_keys(c):
|
|
main_by_email.setdefault(email, []).append(c)
|
|
|
|
# Categorize temp contacts
|
|
matched_by_name = []
|
|
matched_by_email = []
|
|
unmatched = []
|
|
blank = []
|
|
|
|
for c in temp_contacts:
|
|
key = make_key(c)
|
|
emails = make_email_keys(c)
|
|
|
|
if not key and not emails:
|
|
blank.append(c)
|
|
continue
|
|
|
|
if key and key in main_by_name:
|
|
matched_by_name.append((c, main_by_name[key]))
|
|
continue
|
|
|
|
email_match = None
|
|
for email in emails:
|
|
if email in main_by_email:
|
|
email_match = main_by_email[email]
|
|
break
|
|
|
|
if email_match:
|
|
matched_by_email.append((c, email_match))
|
|
else:
|
|
unmatched.append(c)
|
|
|
|
print(f"\n{'='*60}")
|
|
print(f"MERGE ANALYSIS")
|
|
print(f"{'='*60}")
|
|
print(f"Main Contacts folder: {len(main_contacts)}")
|
|
print(f"Temp folder (iCloud): {len(temp_contacts)}")
|
|
print(f"")
|
|
print(f"Matched by name: {len(matched_by_name)}")
|
|
print(f"Matched by email: {len(matched_by_email)}")
|
|
print(f"Unmatched (new): {len(unmatched)}")
|
|
print(f"Blank (no name/email):{len(blank)}")
|
|
print(f"Total categorized: {len(matched_by_name)+len(matched_by_email)+len(unmatched)+len(blank)}")
|
|
|
|
# Analyze what fields the temp contacts have that main doesn't
|
|
print(f"\n{'='*60}")
|
|
print(f"FIELD ANALYSIS - What Temp contacts add")
|
|
print(f"{'='*60}")
|
|
|
|
fields_to_check = ['personalNotes', 'companyName', 'jobTitle', 'birthday',
|
|
'homeAddress', 'businessAddress', 'homePhones', 'businessPhones',
|
|
'nickName', 'categories']
|
|
|
|
for field in fields_to_check:
|
|
temp_has = sum(1 for c in temp_contacts if c.get(field) and
|
|
(isinstance(c[field], str) and c[field].strip() or
|
|
isinstance(c[field], list) and len(c[field]) > 0 or
|
|
isinstance(c[field], dict) and any(v for v in c[field].values())))
|
|
main_has = sum(1 for c in main_contacts if c.get(field) and
|
|
(isinstance(c[field], str) and c[field].strip() or
|
|
isinstance(c[field], list) and len(c[field]) > 0 or
|
|
isinstance(c[field], dict) and any(v for v in c[field].values())))
|
|
print(f" {field:25s}: Temp={temp_has:5d} Main={main_has:5d}")
|
|
|
|
# For matched contacts, how many have notes in temp but not in main?
|
|
notes_to_merge = 0
|
|
for temp_c, main_matches in matched_by_name + matched_by_email:
|
|
temp_notes = (temp_c.get('personalNotes') or '').strip()
|
|
if temp_notes:
|
|
main_notes = (main_matches[0].get('personalNotes') or '').strip()
|
|
if not main_notes:
|
|
notes_to_merge += 1
|
|
|
|
print(f"\n Matched contacts where Temp has notes but Main doesn't: {notes_to_merge}")
|
|
|
|
# Sample unmatched
|
|
print(f"\n{'='*60}")
|
|
print(f"SAMPLE UNMATCHED (first 30 - these would be ADDED to main)")
|
|
print(f"{'='*60}")
|
|
for c in unmatched[:30]:
|
|
name = c.get('displayName', '(no name)')
|
|
emails = ', '.join(e.get('address','') for e in c.get('emailAddresses',[]))
|
|
company = c.get('companyName', '')
|
|
detail = emails or company or ''
|
|
print(f" {name}" + (f" - {detail}" if detail else ""))
|
|
if len(unmatched) > 30:
|
|
print(f" ... and {len(unmatched)-30} more")
|
|
|
|
# Sample blank
|
|
if blank:
|
|
print(f"\n{'='*60}")
|
|
print(f"BLANK CONTACTS ({len(blank)} - no displayName or email)")
|
|
print(f"{'='*60}")
|
|
for c in blank[:10]:
|
|
# Show whatever fields they have
|
|
non_empty = {k: v for k, v in c.items()
|
|
if v and k not in ('id', 'parentFolderId', '@odata.etag', 'changeKey',
|
|
'createdDateTime', 'lastModifiedDateTime', 'categories',
|
|
'flag', 'emailAddresses', 'homePhones', 'businessPhones',
|
|
'imAddresses')
|
|
and not k.startswith('@')}
|
|
print(f" Fields: {list(non_empty.keys())}")
|