Generate GoFlo Test Data #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Generate GoFlo Test Data | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| reference_date: | |
| description: "End date for generated data (YYYY-MM-DD). Leave blank to use today." | |
| required: false | |
| default: "" | |
| jobs: | |
| generate: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Generate test data JSON | |
| run: | | |
| python3 << 'PYEOF' | |
| import json | |
| import calendar | |
| from datetime import date, timedelta | |
| # ── Date window ────────────────────────────────────────────────────── | |
| ref_str = "${{ inputs.reference_date }}".strip() | |
| today = date.fromisoformat(ref_str) if ref_str else date.today() | |
| def subtract_months(d, n): | |
| m = d.month - n | |
| y = d.year | |
| while m <= 0: | |
| m += 12 | |
| y -= 1 | |
| return date(y, m, min(d.day, calendar.monthrange(y, m)[1])) | |
| window_start = subtract_months(today, 3) | |
| # ── Categories ─────────────────────────────────────────────────────── | |
| # 2 of each type: | |
| # default → Symptoms (system), Mood (custom) | |
| # numeric_slider → Flow (system), Brain Fog (custom) | |
| # increment → Pads Used (custom), Medication (custom) | |
| # numeric_free → Outbursts (custom), Energy (custom) | |
| categories = [ | |
| { | |
| "id": 1, "name": "Flow", "isSystem": True, "systemKey": "flow", | |
| "displayOrder": 0, "iconName": "water", "colorToken": "primary", | |
| "categoryType": "numeric_slider", | |
| "numericMin": 1, "numericMax": 4, "allowDecimals": False, | |
| "numericUnit": "", | |
| "scaleLabels": "1=Spotting\n2=Light\n3=Medium\n4=Heavy", | |
| "isArchived": False, "allowMultiple": False, | |
| "showInLogPeriod": False, "trackAgainstTime": True, | |
| "values": ["Spotting", "Light", "Medium", "Heavy"], | |
| }, | |
| { | |
| "id": 2, "name": "Symptoms", "isSystem": True, "systemKey": "symptoms", | |
| "displayOrder": 1, "iconName": "meditation", "colorToken": "secondary", | |
| "categoryType": "default", | |
| "numericMin": 0, "numericMax": 10, "allowDecimals": False, | |
| "numericUnit": "", "scaleLabels": "", | |
| "isArchived": False, "allowMultiple": False, | |
| "showInLogPeriod": False, "trackAgainstTime": False, | |
| "values": [ | |
| "Cramps", "Headache", "Bloating", | |
| "Fatigue", "Back Pain", "Bleeding (non-period)", | |
| ], | |
| }, | |
| { | |
| "id": 3, "name": "Mood", "isSystem": False, "systemKey": "", | |
| "displayOrder": 2, "iconName": "psychology", "colorToken": "tertiary", | |
| "categoryType": "default", | |
| "numericMin": 0, "numericMax": 10, "allowDecimals": False, | |
| "numericUnit": "", "scaleLabels": "", | |
| "isArchived": False, "allowMultiple": True, | |
| "showInLogPeriod": True, "trackAgainstTime": True, | |
| "values": ["Happy", "Sad", "Anxious", "Calm", "Irritable", "Content", "Grumpy"], | |
| }, | |
| { | |
| "id": 6, "name": "Pads Used", "isSystem": False, "systemKey": "", | |
| "displayOrder": 3, "iconName": "healing", "colorToken": "FFF7A7DD", | |
| "categoryType": "increment", | |
| "numericMin": 0, "numericMax": 10, "allowDecimals": False, | |
| "numericUnit": "", "scaleLabels": "", | |
| "isArchived": False, "allowMultiple": False, | |
| "showInLogPeriod": True, "trackAgainstTime": True, | |
| "values": [], | |
| }, | |
| { | |
| "id": 7, "name": "Outbursts", "isSystem": False, "systemKey": "", | |
| "displayOrder": 4, "iconName": "bolt", "colorToken": "FFE53935", | |
| "categoryType": "numeric_free", | |
| "numericMin": 0, "numericMax": 20, "allowDecimals": False, | |
| "numericUnit": "explosions", "scaleLabels": "", | |
| "isArchived": False, "allowMultiple": True, | |
| "showInLogPeriod": True, "trackAgainstTime": True, | |
| "values": [], | |
| }, | |
| { | |
| "id": 8, "name": "Pads bled through", "isSystem": False, "systemKey": "", | |
| "displayOrder": 5, "iconName": "medication", "colorToken": "secondary", | |
| "categoryType": "increment", | |
| "numericMin": 0, "numericMax": 10, "allowDecimals": False, | |
| "numericUnit": "", "scaleLabels": "", | |
| "isArchived": False, "allowMultiple": False, | |
| "showInLogPeriod": True, "trackAgainstTime": False, | |
| "values": [], | |
| }, | |
| { | |
| "id": 10, "name": "Brain Fog", "isSystem": False, "systemKey": "", | |
| "displayOrder": 6, "iconName": "cloud", "colorToken": "FF8A8A8A", | |
| "categoryType": "numeric_slider", | |
| "numericMin": 1, "numericMax": 3, "allowDecimals": False, | |
| "numericUnit": "", | |
| "scaleLabels": "1=Good\n2=Confused\n3=What's my name again?", | |
| "isArchived": False, "allowMultiple": True, | |
| "showInLogPeriod": True, "trackAgainstTime": True, | |
| "values": [], | |
| }, | |
| { | |
| "id": 11, "name": "Energy", "isSystem": False, "systemKey": "", | |
| "displayOrder": 7, "iconName": "fitness", "colorToken": "FFFF9800", | |
| "categoryType": "numeric_free", | |
| "numericMin": 0, "numericMax": 100, "allowDecimals": False, | |
| "numericUnit": "%", "scaleLabels": "", | |
| "isArchived": False, "allowMultiple": False, | |
| "showInLogPeriod": True, "trackAgainstTime": True, | |
| "values": [], | |
| }, | |
| ] | |
| # ── Periods ────────────────────────────────────────────────────────── | |
| # Generate realistic ~28–32 day cycles across the 3-month window. | |
| # Each row: (cycle_length, period_duration, overall_flow, symptoms_list) | |
| cycle_specs = [ | |
| (29, 6, "Heavy", ["Cramps", "Headache"]), | |
| (28, 5, "Medium", ["Cramps", "Bloating", "Fatigue"]), | |
| (31, 7, "Heavy", ["Back Pain", "Headache", "Bloating"]), | |
| (27, 4, "Light", ["Fatigue"]), | |
| ] | |
| # Flow by day-of-period (1-indexed); default Spotting after day 7 | |
| flow_by_day = {1: "4", 2: "4", 3: "4", 4: "3", 5: "2", 6: "1", 7: "1"} | |
| # Pads used per flow level (string, since it's increment/numeric) | |
| pads_by_flow = {"4": 4, "3": 3, "2": 2, "1": 1} | |
| bled_by_flow = {"4": 2, "3": 1, "2": 0, "1": 0} | |
| periods = [] | |
| period_days = set() # all dates covered by any period | |
| tracking_logs = {cat["id"]: [] for cat in categories} | |
| period_start = window_start | |
| spec_idx = 0 | |
| pid = 1 | |
| while period_start <= today: | |
| cycle_len, duration, flow_label, symptoms = cycle_specs[spec_idx % len(cycle_specs)] | |
| period_end_d = period_start + timedelta(days=duration - 1) | |
| ongoing = period_end_d > today | |
| periods.append({ | |
| "id": pid, | |
| "startDate": period_start.isoformat(), | |
| "endDate": None if ongoing else period_end_d.isoformat(), | |
| "flowLevel": flow_label, | |
| "notes": "", | |
| "symptoms": symptoms, | |
| }) | |
| log_end = today if ongoing else period_end_d | |
| d = period_start | |
| day_num = 1 | |
| while d <= log_end: | |
| period_days.add(d) | |
| flow_val = flow_by_day.get(day_num, "1") | |
| # Flow (numeric_slider: log as numeric string "1"–"4") | |
| tracking_logs[1].append({"date": d.isoformat(), "values": [flow_val], "notes": ""}) | |
| # Pads Used (increment: count as string) | |
| base = pads_by_flow.get(flow_val, 1) | |
| pads = max(0, base + (day_num % 3) - 1) | |
| tracking_logs[6].append({"date": d.isoformat(), "values": [str(pads)], "notes": ""}) | |
| # Pads bled through (increment): mostly on heavy days | |
| bled = bled_by_flow.get(flow_val, 0) | |
| if day_num % 2 == 0: | |
| bled = max(0, bled - 1) | |
| tracking_logs[8].append({"date": d.isoformat(), "values": [str(bled)], "notes": ""}) | |
| d += timedelta(days=1) | |
| day_num += 1 | |
| pid += 1 | |
| spec_idx += 1 | |
| period_start += timedelta(days=cycle_len) | |
| # ── Symptoms (default, allowMultiple=False in system category) ─────── | |
| # Log on first 2 period days and 3 pre-menstrual days. | |
| for p in periods: | |
| ps = date.fromisoformat(p["startDate"]) | |
| pe = date.fromisoformat(p["endDate"]) if p["endDate"] else today | |
| # Pre-menstrual (3 days before start) | |
| for offset in (3, 2, 1): | |
| pms = ps - timedelta(days=offset) | |
| if pms >= window_start: | |
| tracking_logs[2].append({ | |
| "date": pms.isoformat(), | |
| "values": ["Bloating", "Cramps"], | |
| "notes": "", | |
| }) | |
| # During period: symptoms on days 1–3 | |
| syms = p["symptoms"] | |
| d = ps | |
| for day_num in range(1, 4): | |
| if d > pe: | |
| break | |
| tracking_logs[2].append({ | |
| "date": d.isoformat(), | |
| # Day 1: all symptoms; Day 2: first two; Day 3: first only | |
| "values": syms[:max(1, len(syms) - day_num + 1)], | |
| "notes": "", | |
| }) | |
| d += timedelta(days=1) | |
| # ── Mood (default, allowMultiple=True) ─────────────────────────────── | |
| single_moods = ["Happy", "Calm", "Content", "Sad", "Anxious", "Irritable", "Grumpy"] | |
| multi_moods = [ | |
| ["Happy", "Calm"], | |
| ["Sad", "Anxious"], | |
| ["Irritable", "Grumpy"], | |
| ["Content", "Happy"], | |
| ] | |
| d = window_start | |
| idx = 0 | |
| while d <= today: | |
| if idx % 2 == 0: # log every other day | |
| if d in period_days: | |
| moods = multi_moods[2] if idx % 4 < 2 else ["Grumpy"] | |
| elif idx % 7 < 2: | |
| moods = multi_moods[0] | |
| elif idx % 7 < 4: | |
| moods = [single_moods[idx % len(single_moods)]] | |
| else: | |
| moods = multi_moods[idx % len(multi_moods)] | |
| tracking_logs[3].append({"date": d.isoformat(), "values": moods, "notes": ""}) | |
| d += timedelta(days=1) | |
| idx += 1 | |
| # ── Outbursts (numeric_free, allowMultiple=True) ───────────────────── | |
| # Higher counts during period; low counts outside. | |
| d = window_start | |
| idx = 0 | |
| while d <= today: | |
| if d in period_days and idx % 2 == 0: | |
| val = 3 + (idx % 5) # 3–7 on period days | |
| tracking_logs[7].append({"date": d.isoformat(), "values": [str(val)], "notes": ""}) | |
| elif idx % 5 == 0 and d not in period_days: | |
| val = idx % 3 # 0–2 outside period | |
| tracking_logs[7].append({"date": d.isoformat(), "values": [str(val)], "notes": ""}) | |
| d += timedelta(days=1) | |
| idx += 1 | |
| # ── Brain Fog (numeric_slider 1–3, allowMultiple=True) ─────────────── | |
| # Every 3 days; max fog during period. | |
| d = window_start | |
| idx = 0 | |
| while d <= today: | |
| if idx % 3 == 0: | |
| if d in period_days: | |
| fog = "3" | |
| elif idx % 9 < 3: | |
| fog = "1" | |
| elif idx % 9 < 6: | |
| fog = "2" | |
| else: | |
| fog = "3" | |
| tracking_logs[10].append({"date": d.isoformat(), "values": [fog], "notes": ""}) | |
| d += timedelta(days=1) | |
| idx += 1 | |
| # ── Energy (numeric_free, allowMultiple=False) ──────────────────────── | |
| # Daily; dips during period, peaks mid-cycle. | |
| d = window_start | |
| idx = 0 | |
| while d <= today: | |
| if d in period_days: | |
| energy = 20 + (idx % 20) # 20–39 % | |
| elif idx % 14 in (6, 7): # mid-cycle high | |
| energy = 85 + (idx % 15) # 85–99 % | |
| else: | |
| energy = 50 + (idx % 35) # 50–84 % | |
| energy = min(energy, 100) | |
| tracking_logs[11].append({"date": d.isoformat(), "values": [str(energy)], "notes": ""}) | |
| d += timedelta(days=1) | |
| idx += 1 | |
| # ── Assemble tracking array ─────────────────────────────────────────── | |
| cat_by_id = {c["id"]: c for c in categories} | |
| tracking = [ | |
| { | |
| "id": cid, | |
| "name": cat_by_id[cid]["name"], | |
| "archived": False, | |
| "type": cat_by_id[cid]["categoryType"], | |
| "logs": logs, | |
| } | |
| for cid, logs in tracking_logs.items() | |
| if logs | |
| ] | |
| # ── Pinned stats that exercise chart types ──────────────────────────── | |
| # (stored as a JSON string, matching the export format) | |
| pinned = [ | |
| { | |
| "id": "1__TIME_SERIES_MONTH", | |
| "label": "Flow", | |
| "categoryId1": 1, | |
| "timeRangeType": "MONTH", | |
| "chartType": "TIME_SERIES", | |
| }, | |
| { | |
| "id": "2__PIE_ALL_TIME", | |
| "label": "Symptoms", | |
| "categoryId1": 2, | |
| "timeRangeType": "ALL_TIME", | |
| "chartType": "PIE", | |
| }, | |
| { | |
| "id": "6_8_DUAL_TIME_SERIES_MONTH", | |
| "label": "Pads Used vs Pads bled through", | |
| "categoryId1": 6, | |
| "categoryId2": 8, | |
| "timeRangeType": "MONTH", | |
| "chartType": "DUAL_TIME_SERIES", | |
| }, | |
| { | |
| "id": "11__TIME_SERIES_MONTH", | |
| "label": "Energy", | |
| "categoryId1": 11, | |
| "timeRangeType": "MONTH", | |
| "chartType": "TIME_SERIES", | |
| }, | |
| ] | |
| export = { | |
| "version": 3, | |
| "exportedAt": today.isoformat(), | |
| "fullBackup": True, | |
| "dateRange": { | |
| "from": window_start.isoformat(), | |
| "to": today.isoformat(), | |
| }, | |
| "categories": categories, | |
| "pinnedStats": json.dumps(pinned), | |
| "periods": periods, | |
| "tracking": tracking, | |
| } | |
| filename = f"goflo_test_data_{today.isoformat()}.json" | |
| with open(filename, "w") as fh: | |
| json.dump(export, fh, indent=2) | |
| # Summary | |
| print(f"Window : {window_start} → {today}") | |
| print(f"Periods: {len(periods)}") | |
| for cat in categories: | |
| logs = tracking_logs[cat["id"]] | |
| print(f" [{cat['categoryType']:16s}] {cat['name']:20s} {len(logs):3d} logs") | |
| print(f"Output : {filename}") | |
| PYEOF | |
| - name: Upload test data artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: goflo-test-data-${{ inputs.reference_date || 'today' }} | |
| path: goflo_test_data_*.json | |
| retention-days: 30 |