#!/usr/bin/env bash # Pulls today's campaign snapshot from the source Google Sheet and writes # data.json + history.json for the report page. # # The source spreadsheet creates a fresh tab per day named "M/D" (e.g. 9/9). # We fetch via the gviz CSV endpoint with URL-encoded sheet name and walk # back up to 7 days if today's tab hasn't been created yet. set -euo pipefail SHEET_ID="1t2Sz2W6845HbS1ZGJN1iuaj9cgjPFHt36U1fK938jT8" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CSV_FILE="$SCRIPT_DIR/.sheet.csv" log() { echo "[update.sh $(date -u +%FT%TZ)] $*" >&2; } # Compute M/D for a day offset (portable via python3). mmdd_of() { python3 -c "import datetime,sys;d=datetime.date.today()+datetime.timedelta(days=int(sys.argv[1]));print(f'{d.month}/{d.day}')" "$1" } # Fetch a candidate tab into CSV_FILE. Success = first header column is "Order ID" # (gviz silently returns the first tab when the requested name doesn't exist). fetch_sheet() { local name="$1" local encoded="${name//\//%2F}" local url="https://docs.google.com/spreadsheets/d/${SHEET_ID}/gviz/tq?tqx=out:csv&sheet=${encoded}" if ! curl --max-time 60 -sSL -f "$url" -o "$CSV_FILE"; then return 1 fi local first_col first_col=$(head -1 "$CSV_FILE" | tr -d '\r' | cut -d',' -f1 | tr -d '"') [ "$first_col" = "Order ID" ] } SHEET_NAME="" for offset in 0 -1 -2 -3 -4 -5 -6 -7; do candidate=$(mmdd_of "$offset") log "trying sheet '$candidate'" if fetch_sheet "$candidate"; then SHEET_NAME="$candidate" log "matched sheet '$candidate'" break fi done if [ -z "$SHEET_NAME" ]; then log "no matching sheet found in the last 8 days" rm -f "$CSV_FILE" exit 1 fi SHEET_NAME="$SHEET_NAME" SCRIPT_DIR="$SCRIPT_DIR" CSV_FILE="$CSV_FILE" python3 <<'PYEOF' import csv, json, os, sys, datetime script_dir = os.environ['SCRIPT_DIR'] csv_file = os.environ['CSV_FILE'] sheet_name = os.environ['SHEET_NAME'] data_path = os.path.join(script_dir, 'data.json') hist_path = os.path.join(script_dir, 'history.json') with open(csv_file, newline='', encoding='utf-8') as f: raw = list(csv.reader(f)) if not raw: print('empty CSV', file=sys.stderr); sys.exit(1) headers = [h.strip() for h in raw[0]] records = [] for r in raw[1:]: if not any((c or '').strip() for c in r): continue row = {} for i, h in enumerate(headers): row[h] = (r[i].strip() if i < len(r) else '') records.append(row) def num(x): if x is None: return 0.0 s = str(x).replace(',', '').replace('$', '').replace('%', '').strip() if not s: return 0.0 try: return float(s) except ValueError: return 0.0 # Aggregate totals for the summary strip. sum_keys = ['Valid Click', 'PB Installs', 'Revenue', 'Cost', 'Gross Profit', 'RT Reject Installs'] totals = {k: round(sum(num(r.get(k, '')) for r in records), 4) for k in sum_keys if k in headers} # Derived rates (recomputed from totals, not averaged across rows). if totals.get('Valid Click', 0) > 0 and 'PB Installs' in totals: totals['CVR'] = round(totals['PB Installs'] / totals['Valid Click'], 6) if totals.get('Revenue', 0) > 0 and 'Gross Profit' in totals: totals['%Gross Profit'] = round(totals['Gross Profit'] / totals['Revenue'], 6) if 'PB Installs' in totals and 'RT Reject Installs' in totals: denom = totals['PB Installs'] + totals['RT Reject Installs'] if denom > 0: totals['%RT Reject Install'] = round(totals['RT Reject Installs'] / denom, 6) # Distributions for the mini-charts. def bucket(field): out = {} for r in records: key = (r.get(field) or '').strip() or 'Unknown' out[key] = out.get(key, 0) + 1 return out status_counts = bucket('Status') if 'Status' in headers else {} geo_counts = bucket('Geo') if 'Geo' in headers else {} # Top-10 by revenue for the campaign bar chart. def top_by(field, n=10): scored = [(num(r.get(field, '')), r) for r in records] scored.sort(key=lambda t: t[0], reverse=True) out = [] for val, r in scored[:n]: if val <= 0: break out.append({ 'campaign': r.get('Campaign Name', '') or r.get('Campaign ID', ''), 'pkg': r.get('PKG Name', ''), 'geo': r.get('Geo', ''), 'value': val, 'revenue': num(r.get('Revenue', '')), 'cost': num(r.get('Cost', '')), 'gp': num(r.get('Gross Profit', '')), }) return out fetched_at = datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC') payload = { 'sheet_name': sheet_name, 'fetched_at': fetched_at, 'columns': headers, 'row_count': len(records), 'totals': totals, 'status_counts': status_counts, 'geo_counts': geo_counts, 'top_by_revenue': top_by('Revenue'), 'top_by_gp': top_by('Gross Profit'), 'rows': records, } with open(data_path, 'w', encoding='utf-8') as f: json.dump(payload, f, ensure_ascii=False, indent=2) # Keep a rolling history of daily totals so the trend chart has time series. try: with open(hist_path, 'r', encoding='utf-8') as f: history = json.load(f) if not isinstance(history, list): history = [] except (FileNotFoundError, json.JSONDecodeError): history = [] history = [h for h in history if h.get('sheet_name') != sheet_name] history.append({ 'sheet_name': sheet_name, 'fetched_at': fetched_at, 'row_count': len(records), 'totals': totals, 'status_counts': status_counts, }) def sort_key(h): try: m, d = h['sheet_name'].split('/') return (int(m), int(d)) except Exception: return (0, 0) history.sort(key=sort_key) history = history[-120:] # keep ~4 months with open(hist_path, 'w', encoding='utf-8') as f: json.dump(history, f, ensure_ascii=False, indent=2) print(f'wrote {len(records)} rows for sheet {sheet_name}; history now {len(history)} days', file=sys.stderr) PYEOF rm -f "$CSV_FILE" log "done"