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
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user