sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00
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>
This commit is contained in:
217
temp/bardach_merge_analysis.py
Normal file
217
temp/bardach_merge_analysis.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""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())}")
|
||||
Reference in New Issue
Block a user