Files
routing--/update.sh
T
zhangyujia 12ca4a4af7 Initial campaign daily report
- index.html: table + trend chart, powered by Chart.js
- update.sh: fetches today's tab from source Google Sheet (falls back up to 7 days)
- schedule.conf: refresh every 24h
- data.json / history.json: initial snapshot from sheet 9/9 so the page has data before the first scheduled run
2026-09-10 16:39:50 +08:00

178 lines
5.9 KiB
Bash
Executable File

#!/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"