Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions wing-command/.env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,7 @@
# ===========================================
# Wing Command — Environment Variables
# ===========================================
# TinyFish Web Agent API key
# Get yours at: https://agent.tinyfish.ai/api-keys
TINYFISH_API_KEY="your_api_key_here"

# Supabase (required) — https://supabase.com/dashboard
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

# TinyFish Scraper (required) — https://docs.tinyfish.ai
TINYFISH_API_KEY=your-tinyfish-api-key
# TINYFISH_API_URL=https://agent.tinyfish.ai/v1/automation/run # sync (default)
# TINYFISH_API_URL=https://agent.tinyfish.ai/v1/automation/run-sse # SSE streaming

# Upstash Redis (optional, recommended) — https://upstash.com
# UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
# UPSTASH_REDIS_REST_TOKEN=your-redis-token
# Groq API key
# Get yours at: https://console.groq.com
GROQ_API_KEY="your_api_key_here"
3 changes: 2 additions & 1 deletion wing-command/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ yarn-debug.log*
yarn-error.log*

# Local env files
.env*.local
.env
.env.local
.env.*.local

# Vercel
.vercel
Expand Down
217 changes: 93 additions & 124 deletions wing-command/README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,62 @@
# Wing Scout - Super Bowl LX War Room
# Wing Command
**Live Demo: https://wings-command.up.railway.app/**

**Flavor-first, mesmerizing, hyper-local chicken wing tracker for Super Bowl LX (Feb 8, 2026).**
**Flavor-first, hyper-local chicken wing tracker powered by TinyFish**

A "War Room" interface that scouts the best chicken wings near you in real-time using AI-powered parallel scraping from DoorDash, Uber Eats, Grubhub, and Google.
A "War Room" interface that scouts the best chicken wings near you in real-time. Pick a flavor persona, enter your zip, and parallel AI browser agents hit DoorDash, Uber Eats, Grubhub, and Google simultaneously — streaming results back as each source completes.

## Visual Identity

- **Theme:** "Midnight Turf" - Dark #050505 background with subtle grass texture grid
- **Accents:** Neon Green (#39FF14) glows, stadium lighting effects
- **Typography:** Bebas Neue (scoreboard style) + Inter (body)
- **Cards:** Glassmorphism "Scout Cards" with backdrop blur
- **Animations:** Framer Motion parallax, floating particles, "tackle-in" card entrances
- **Theme:** "Midnight Turf" — dark `#050505` background with subtle grass texture grid
- **Accents:** Neon Green (`#39FF14`) glows, stadium lighting effects
- **Typography:** Bebas Neue (scoreboard) + Inter (body)
- **Cards:** Glassmorphism Scout Cards with backdrop blur
- **Animations:** Framer Motion parallax, floating particles, tackle-in card entrances

## Architecture

```
Frontend (Next.js 14 App Router)
|-- page.tsx -> War Room hero + flavor selector + results grid
|-- HeroVisuals.tsx -> Parallax field lines + floating wing/confetti particles
|-- FlavorSelector.tsx -> 3 flavor personas with pulsing selection
|-- ZipSearch.tsx -> Stadium-light illuminated zip input
|-- WingGrid.tsx -> Glassmorphism Scout Cards grid

Backend (API Route)
|-- /api/scout -> Main endpoint (zip + flavor)
|-- lib/tinyfish-scraper.ts -> TinyFish parallel scraping engine
|-- lib/geocode.ts -> Nominatim (OpenStreetMap) geocoding
|-- lib/cache.ts -> Upstash Redis caching layer

Data
|-- Supabase (PostgreSQL + PostGIS)
|-- Upstash Redis (15-min TTL cache)
┌─────────────────────────────────────────────────────────┐
│ Browser (Client) │
│ │
│ FlavorSelector → ZipSearch → WingGrid (streams in) │
└──────────────────────────┬──────────────────────────────┘
│ GET /api/scout?zip=&flavor=
│ (SSE — results stream as agents finish)
┌──────────────────────────▼──────────────────────────────┐
│ Next.js App Router │
│ │
│ /api/scout/route.ts │
│ │ │
│ ├─ 1. geocode.ts ──► Nominatim (free, no key) │
│ │ │
│ ├─ 2. Groq LLM ────► generates 5–7 source URLs │
│ │ llama-3.3-70b-versatile │
│ │ │
│ └─ 3. TinyFish SDK ─► Promise.allSettled │
│ client.agent.stream({ url, goal }) │
│ │ │
│ ├── Agent → DoorDash city wings page │
│ ├── Agent → Uber Eats city wings page │
│ ├── Agent → Grubhub city wings page │
│ └── Agent → Google search results │
│ │
│ Each agent fires EventType.COMPLETE │
│ → spots sent to client immediately via SSE │
└─────────────────────────────────────────────────────────┘

No database. No cache. Pure in-memory — results live only for the request.
```

### TinyFish SDK event flow

```
client.agent.stream({ url, goal })
├── EventType.STARTED → agent confirmed running
├── EventType.PROGRESS → live step updates (shown in UI)
└── EventType.COMPLETE
└── RunStatus.COMPLETED → event.result → parse restaurants → SSE → client
```

## Flavor Personas
Expand All @@ -44,68 +69,46 @@ Users pick a team/flavor before searching:
| **The Classicist** | Buffalo, Hot, Mild, Traditional, Cayenne | 🦬 |
| **The Sticky Finger** | Honey BBQ, Garlic Parm, Teriyaki, Korean | 🍯 |

Spots are scored 0-100 against the selected persona.
Spots are scored 0100 against the selected persona.

## Scraping Flow

1. User enters ZIP + selects Flavor Persona
2. **Geocode** via Nominatim (free, no API key)
3. **Parallel scrape** (`Promise.allSettled`):
- DoorDash search results
- Uber Eats search results
- Grubhub search results
- Google search (hidden gem detection)
4. **Deduplicate** by normalized name + address
5. **Score** against flavor persona keywords
6. **Cache** in Redis (15-min TTL) + persist to Supabase
3. **Groq** (`llama-3.3-70b-versatile`) generates 5–7 real search URLs for the location
4. **Parallel scrape** (`Promise.allSettled`) — one TinyFish browser agent per URL:
- DoorDash city wings page
- Uber Eats city wings page
- Grubhub city wings page
- Google search results
5. **Deduplicate** by normalized name + address
6. **Score** against flavor persona keywords
7. Results stream back to the client as each agent completes

## Setup

### Prerequisites

- Node.js >= 18
- Supabase project (free tier works)
- Node.js 22.x
- TinyFish API key
- Upstash Redis (optional but recommended)
- Groq API key

### Environment Variables

Create `.env.local`:

```env
# Required - Server
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
TINYFISH_API_KEY=your-tinyfish-api-key

# Required - Client
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key

# Optional - Caching (highly recommended)
UPSTASH_REDIS_REST_URL=https://your-redis.upstash.io
UPSTASH_REDIS_REST_TOKEN=your_redis_token

# Optional - Custom TinyFish endpoint
TINYFISH_API_URL=https://agent.tinyfish.ai/v1/automation/run
```bash
cp .env.example .env.local
```

### Database Setup
Then fill in:

Run the schema in your Supabase SQL editor:
```env
# TinyFish (required) — https://agent.tinyfish.ai/api-keys
TINYFISH_API_KEY=your-tinyfish-api-key

```bash
# The schema file is at:
supabase/schema.sql
# Groq (required) — https://console.groq.com
GROQ_API_KEY=your-groq-api-key
```

This creates:
- `wing_spots` table with `flavor_tags` (TEXT[]), `menu_json` (JSONB), and PostGIS spatial indexing
- `geocode_cache` table (permanent zip-to-lat/lng mapping)
- `scrape_queue` table (for background cron jobs)
- `menus` table (cached restaurant menus)
- PostGIS trigger for auto-computing `location` from `lat`/`lng`
- Row Level Security policies

### Install & Run

```bash
Expand All @@ -115,97 +118,64 @@ npm run dev

Open http://localhost:3000

## Render.com Deployment (Migration from Vercel)
## Deployment

### Why Render?
The app is deployed on Railway. Any Node-compatible host works — no external services to provision.

Vercel serverless functions have a **60-second timeout** (300s on Hobby with Fluid Compute). Parallel scraping across 4 platforms can exceed this. Render Web Services have **no timeout limit**.
### Render.com (alternative)

### Render Setup
Vercel serverless functions have a 60-second timeout. Parallel scraping across 4 platforms can exceed this. Render Web Services have no timeout limit.

1. **Create a Web Service** on Render
- Connect your GitHub repository
1. Create a Web Service — connect your GitHub repo
- **Build Command:** `npm install && npm run build`
- **Start Command:** `npm start`
- **Environment:** Node
- **Plan:** Starter ($7/mo) or Free (with limitations)

2. **Environment Variables**
- Add all env vars from the `.env.local` section above
- Set `NODE_ENV=production`

3. **Render Config (`render.yaml`)**
2. Add environment variables:

```yaml
services:
- type: web
name: wing-scout
name: wing-command
runtime: node
buildCommand: npm install && npm run build
startCommand: npm start
envVars:
- key: NODE_ENV
value: production
- key: NEXT_PUBLIC_SUPABASE_URL
sync: false
- key: NEXT_PUBLIC_SUPABASE_ANON_KEY
sync: false
- key: SUPABASE_SERVICE_ROLE_KEY
sync: false
- key: TINYFISH_API_KEY
sync: false
- key: UPSTASH_REDIS_REST_URL
sync: false
- key: UPSTASH_REDIS_REST_TOKEN
- key: GROQ_API_KEY
sync: false
```

4. **Next.js Standalone Output**
- `next.config.mjs` includes `output: 'standalone'` which is required for Render deployment

### Cron Job (Optional)

Set up a Render Cron Job to pre-populate the database:

- **Schedule:** `0 */4 * * *` (every 4 hours)
- **Command:** `python scraper/scrape_wings.py`
- This pre-scrapes 150+ major US zip codes
`next.config.mjs` already includes `output: 'standalone'` for Render compatibility.

## Project Structure

```
wing-scout/
wing-command/
├── app/
│ ├── layout.tsx # Root layout (Bebas Neue + Inter fonts)
│ ├── page.tsx # War Room main page
│ ├── globals.css # Midnight Turf theme styles
│ ├── globals.css # Midnight Turf theme
│ ├── loading.tsx # Loading state
│ ├── error.tsx # Error boundary
│ └── api/
│ ├── scout/route.ts # GET /api/scout?zip=xxxxx&flavor=classicist
│ └── menu/route.ts # GET /api/menu?spot_id=xxxxx
│ └── discover/route.ts # URL discovery via Groq
├── components/
│ ├── HeroVisuals.tsx # Parallax + floating particles (Framer Motion)
│ ├── FlavorSelector.tsx # 3 flavor persona cards with pulse animation
│ ├── ZipSearch.tsx # Stadium-lit glowing zip input
│ ├── WingGrid.tsx # Scout Cards grid (glassmorphism)
│ └── ui/ # Reusable UI primitives
├── lib/
│ ├── tinyfish-scraper.ts # TinyFish scraper (parallel, flavor-aware)
│ ├── types.ts # TypeScript definitions
│ ├── types.ts # TypeScript definitions (pure in-memory, no DB)
│ ├── utils.ts # Flavor scoring, dedup, formatting
│ ├── supabase.ts # Database client
│ ├── cache.ts # Upstash Redis caching
│ ├── geocode.ts # Nominatim geocoding
│ ├── env.ts # Environment validation
│ └── menu.ts # Menu fetching
├── supabase/
│ └── schema.sql # Full database schema
├── scraper/
│ └── scrape_wings.py # Python cron pre-scraper
│ ├── geocode.ts # Nominatim geocoding (no API key needed)
│ └── env.ts # Environment validation
├── tailwind.config.ts # Midnight Turf theme
├── next.config.mjs # Standalone output for Render
└── package.json # framer-motion, lucide-react, etc.
├── next.config.mjs # Standalone output
└── package.json
```

## Constraint Checklist
Expand All @@ -214,18 +184,17 @@ wing-scout/
|-----------|--------|
| Google Places API used? | NO (OSM/Nominatim) |
| Yelp API used? | NO |
| Vercel deployment? | NO (Render.com) |
| External database or cache used? | NO (pure in-memory) |
| UI "Mesmerizing"? | YES (Framer Motion, glassmorphism, neon glow) |
| Menu scraping parallel? | YES (`Promise.allSettled` across 4 sources) |
| Flavor persona filtering? | YES (keyword matching, 0-100 scoring) |
| Scraping parallel? | YES (`Promise.allSettled` across 4–7 sources) |
| Flavor persona filtering? | YES (keyword matching, 0100 scoring) |

## Tech Stack

- **Framework:** Next.js 14 (App Router), TypeScript, Tailwind CSS
- **Animations:** Framer Motion
- **Icons:** Lucide React
- **Database:** Supabase (PostgreSQL + PostGIS)
- **Cache:** Upstash Redis
- **Scraper:** TinyFish
- **Geocoding:** Nominatim (OpenStreetMap)
- **Deployment:** Render.com (Web Service)
- **AI / LLM:** Groq (`llama-3.3-70b-versatile`) for URL discovery
- **Browser Agents:** TinyFish SDK (`client.agent.stream`)
- **Geocoding:** Nominatim (OpenStreetMap) — no API key required
- **Deployment:** Railway / Render.com
Loading