The Core Principle

βœ… After
App reads only from athena_warehouse. Data is fresh as of last nightly sync. Data gaps are visible. Write-back queues to Airtable/Asana via PDOS Bridge.
❌ Before
Products/brands/SKUs call Airtable API directly. Revenue calls FastAPI. Mixed data sources. Blank brands when Airtable linked records are unresolved. No visibility into data gaps.

Warehouse Data Already Available

All data needed for this migration is already in athena_warehouse:

TableRowsSourceSync Frequency
airtable_raw.product160AirtableNightly
airtable_raw.dev_group29AirtableNightly
airtable_raw.sku741AirtableNightly
airtable_raw.product_line13AirtableNightly
airtable_raw.client17Airtable (brands)Nightly
asana_raw.project42AsanaNightly
asana_raw.task462AsanaNightly
ns.invoices325KNetSuiteNightly
pdos.pending_sync_queue2 pendingApp writesPDOS Bridge drains every 5min
⚠️ NS Sync Staleness: netsuite_raw.sync_state shows last sync April 20 (3 weeks ago) despite nightly crons reporting success. This is a separate issue to diagnose β€” Gluon must answer this in the review phase. Revenue data may be stale.

Architecture After This Build

🌐 Browser
product.athenaag.com
↓ CF Pages static HTML/JS
βš™οΈ CF Worker
Asana writes Β· Notion Β· Queue mgmt
Keeps these routes
⚑ FastAPI
pulse.athenaag.com Β· port 5051
All product/brand/SKU/revenue reads move here
↓ psycopg2
πŸ—„ athena_warehouse
PostgreSQL Β· airtable_raw Β· asana_raw Β· ns Β· pdos
↓ Write-back only (async via PDOS queue)
Airtable
Products Β· Dev Groups
Asana
Stage dates Β· Tasks

Phase 1 β€” New FastAPI Endpoints

Add to athena-bi-mvp/backend/ as a new router: routers/pipeline.py.

EndpointSourceKey fields
GET /api/pipeline/products airtable_raw.product + product_line + client id, name, status, brand_name, product_line_name, sku_count, has_data_gaps, data_gaps[]
GET /api/pipeline/brands airtable_raw.client + product + ns.invoices id, name, product_count, sku_count, dev_group_count, revenue_ytd
GET /api/pipeline/product-lines airtable_raw.product_line + product id, name, product_count, active_product_count, dev_group_count
GET /api/pipeline/skus airtable_raw.sku + product id, sku_name, product_name, size_name, regions, photo_url, has_data_gaps
GET /api/pipeline/dev-groups airtable_raw.dev_group + client + product_line All stage dates, asana_gids, brand_name, has_data_gaps
PATCH /api/pipeline/dev-groups/{id} airtable_raw.dev_group β†’ pdos.pending_sync_queue Writes warehouse first, enqueues Airtable + Asana sync. Returns 200 immediately.
GET /api/pipeline/line-badges airtable_raw.product_line id, name, logo_url β€” replaces CF Worker Airtable call
GET /api/pipeline/revenue ns.invoices (existing) ytd_total, monthly[], by_brand[], by_product_line[], data_freshness.is_stale
Data Gap Rules β€” has_data_gaps: true when:
Product: brand_id IS NULL or product_line IS NULL or netsuite_item IS NULL
Dev group: brand IS NULL or product_line IS NULL or launch_date IS NULL
SKU: product_id IS NULL (orphaned)

Phase 2 β€” CF Worker Cleanup

Remove β€” proxy to FastAPI instead

  • GET /api/products β†’ proxy pulse.athenaag.com/api/pipeline/products
  • GET /api/line-badges β†’ proxy pulse.athenaag.com/api/pipeline/line-badges
  • GET /api/revenue β†’ update to pulse.athenaag.com/api/pipeline/revenue

Keep β€” no warehouse equivalent

  • POST /api/tasks β€” Asana task creation
  • GET/POST /api/queue/* β€” action queue
  • POST /api/ask β€” Groq AI
  • GET /api/modal/{id} β€” Notion modal
  • POST /api/activity β€” activity log

Phase 3 β€” Frontend Updates

Minimal frontend changes required β€” the read path changes are transparent since CF Worker proxies the same URLs.

  • Data gap badges β€” when has_data_gaps: true, show ⚠️ badge on product/brand/dev group card with tooltip listing data_gaps[]
  • Revenue staleness banner β€” if data_freshness.is_stale: true, show top banner: "Revenue data may be outdated β€” last sync [date]"
  • Dev group stage editing β€” existing edit UI sends PATCH to CF Worker, which proxies to FastAPI PATCH /api/pipeline/dev-groups/{id}

Sync Frequency Decisions

SourceDirectionFrequencyRationale
NetSuiteInbound β†’ warehouseNightlyRevenue doesn't change intraday in ways that matter for pipeline view
AirtableInbound β†’ warehouseNightlyApp is authoritative writer; inbound only catches external edits
AirtableOutbound (write-back)Every 5min via PDOS queuePDOS Bridge already runs every 5min
AsanaOutbound (stage dates)Every 5min via PDOS queueSame queue, same cron
AsanaInbound β†’ warehouseNightlyTask status is informational on pipeline view

Open Questions β€” Gluon Must Answer Before Build

1
NS Sync Staleness

netsuite_raw.sync_state shows last sync April 20 despite nightly crons reporting success. Why the discrepancy? Is the overnight sync writing to the wrong table? Is revenue data actually 3 weeks stale?

2
PDOS Write-Back Schema

Pull exact column definitions for pdos.pending_sync_queue. What shape does a dev_group write-back entry need? PATCH endpoint inserts here β€” must match the schema exactly.

3
CF Access on pulse.athenaag.com

Is there currently a CF Access policy on pulse.athenaag.com? If the pipeline frontend calls it directly (not via CF Worker), will unauthenticated requests be blocked? Confirm before routing frontend there.

4
Brand-to-Revenue Join

Does the existing BRAND_MAP in main.py match cleanly against airtable_raw.client.name? Any mismatches that would break the /api/pipeline/brands revenue join?

Quasar QA Checklist