Azure-centric proof-of-concept for extracting youth baseball team branding and generating glove design proposals with an optional Playwright autofill worker.
- You send a team website URL.
- The system crawls a few pages, finds the best logo, and extracts colors.
- It creates three glove design variants (A/B/C).
- It stores all outputs in Azure Blob Storage and tracks job status.
- If you choose
autofill, a Playwright worker tries to fill out the BC2 Gloves wizard. If it cannot, the job still finishes with a proposal and manual steps.
Client
|
v
POST /jobs (Functions API) ---> Service Bus queue ---> Durable Orchestrator
|-> validate + robots + crawl
|-> logo scoring + palette
|-> glove variants + proposal
|-> Blob artifacts
|-> Cosmos/Table job status
|
v
Wizard Worker (Playwright)
(HTTP or Service Bus job)
- Quick Reference - Common commands and quick troubleshooting
- Contributing Guide - Setup and development guidelines
- Production Deployment Guide - Complete guide for deploying to Azure, monitoring, alerts, and operational procedures
- Troubleshooting Guide - Solutions for common issues (jobs stuck, dead letters, configuration errors)
- Testing Guide - Unit, integration, and coverage testing
- Customizer Documentation - Interactive glove designer architecture and requirements
- Security Guide - Security considerations and best practices
- Changelog - Version history and release notes
- Node.js 20+
- Azure Functions Core Tools v4
- Azurite (Blob + Table)
npm installnpm run devnpm run worker:devSet WORKER_HEADLESS=false for a visible browser window.
Use UseDevelopmentStorage=true for Blob + Table in local settings. Example local.settings.json values:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"SERVICEBUS_CONNECTION": "<service bus connection string>",
"SERVICEBUS_QUEUE": "glovejobs",
"BLOB_CONNECTION_STRING": "UseDevelopmentStorage=true",
"BLOB_CONTAINER": "glovejobs",
"TABLE_CONNECTION_STRING": "UseDevelopmentStorage=true",
"TABLE_NAME": "jobs",
"WIZARD_ENDPOINT": "http://localhost:7072"
}
}npm run start:cli -- https://arlingtontravelbaseball.org/Artifacts are written to ./local-output.
The front-end scaffolding lives in frontend/ (Vite + React).
cd frontend
npm install
npm run devThe UI uses the shared catalog JSON for now and calls the APIs later.
See docs/CUSTOMIZER.md for the product spec, data model, and option engine notes.
To change the checkout handoff link, set VITE_ORDER_URL before running:
VITE_ORDER_URL=https://aka.ms/myorder npm run devTo enable the team branding scan on the start page, point the UI to the Functions API:
VITE_API_BASE=https://<function-app>.azurewebsites.net VITE_FUNCTION_KEY=<optional> npm run devIn production (SWA), set VITE_API_BASE as a GitHub Actions variable and VITE_FUNCTION_KEY as a secret so the build can inject them.
These are the building blocks in Azure. Think of them like Lego pieces the app snaps together:
- Azure Functions (Function App)
- Runs the API and Durable Functions orchestration.
- Receives
POST /jobsandGET /jobs/{jobId}.
- Azure Service Bus (Queue)
- Holds incoming job messages so work can run in the background.
- Azure Storage Account (Blob)
- Stores the artifacts under
/jobs/{jobId}/.
- Stores the artifacts under
- Cosmos DB (or Table Storage)
- Stores job status and progress.
- Application Insights
- Collects logs and traces. Every log includes
jobId.
- Collects logs and traces. Every log includes
- Container Apps Job (wizard worker)
- Runs Playwright automation (optional).
If you deploy with infra/main.bicep, the template creates all of the above.
Use the Bicep template:
az group create --name glovedesign-rg --location eastus
az deployment group create \
--resource-group glovedesign-rg \
--template-file infra/main.bicep \
--parameters location=eastus projectName=glovedesignThe Function App needs these settings (see table below). Most are filled in by Bicep, but add the ones you need:
- If you want queue-based worker: set
WIZARD_QUEUEandWIZARD_RESULTS_QUEUE. - If you want HTTP worker: set
WIZARD_ENDPOINT.
Use your preferred method, for example:
func azure functionapp publish <function-app-name>The worker can be triggered in two ways:
- HTTP mode: deploy the container and set
WIZARD_ENDPOINTto its URL. - Queue mode: deploy the container and set
WIZARD_QUEUE+WIZARD_RESULTS_QUEUE.
Docker build (example):
docker build -f worker/Dockerfile -t glovedesign-wizard:latest .curl -X POST https://<function-app>.azurewebsites.net/api/jobs \
-H "x-functions-key: <key>" \
-H "content-type: application/json" \
-d '{"teamUrl":"https://arlingtontravelbaseball.org/","mode":"proposal"}'Response:
{ "jobId": "uuid" }curl -X GET https://<function-app>.azurewebsites.net/api/jobs/<jobId> \
-H "x-functions-key: <key>"When the job finishes, status becomes Succeeded or Failed.
Artifacts are written to Blob Storage under:
/jobs/{jobId}/
Look for:
logo.(png|jpg|svg)palette.jsonglove_design.jsonproposal.mdcrawl_report.jsonwizard_schema_snapshot.json(autofill attempted)configured.png(autofill success only)
- API validates the URL (SSRF checks).
- Job is queued in Service Bus.
- Durable Functions runs each stage:
- validate input
- robots + terms check
- crawl pages
- score logo
- extract colors
- generate designs
- write artifacts
- optional autofill worker
- Job status is stored in Cosmos DB or Table Storage.
| Variable | Purpose |
|---|---|
SERVICEBUS_NAMESPACE |
Service Bus namespace (e.g. <name>.servicebus.windows.net) |
SERVICEBUS_QUEUE |
Queue name for job submission (default glovejobs) |
SERVICEBUS_CONNECTION |
Connection string used by Function trigger and optional worker queue |
WIZARD_QUEUE |
Optional wizard worker queue name (event-driven job) |
WIZARD_RESULTS_QUEUE |
Optional wizard results queue (default ${WIZARD_QUEUE}-results) |
COSMOS_ENDPOINT |
Cosmos DB endpoint |
COSMOS_CONNECTION_STRING |
Optional Cosmos connection string (local dev) |
COSMOS_DATABASE |
Cosmos DB database name |
COSMOS_CONTAINER |
Cosmos DB container name |
BLOB_URL |
Storage account Blob endpoint |
BLOB_CONNECTION_STRING |
Optional Blob connection string (local dev) |
BLOB_CONTAINER |
Blob container name |
BLOB_BASE_URL |
Base URL for blob (used by wizard worker) |
TABLE_CONNECTION_STRING |
Optional Table connection string (local dev) |
TABLE_NAME |
Table name (default jobs) |
WIZARD_ENDPOINT |
Optional HTTP endpoint for Playwright worker |
| Variable | Purpose | Default |
|---|---|---|
BRANDING_CRAWL_MAX_PAGES |
Maximum pages to crawl per job | 6 |
BRANDING_CRAWL_MAX_IMAGES |
Maximum image candidates to collect | 40 |
BRANDING_CRAWL_MAX_CSS_FILES |
Maximum CSS files to parse for images | 6 |
BRANDING_CRAWL_MAX_BYTES |
Total download budget per job (bytes) | 26214400 (25MB) |
BRANDING_CRAWL_MAX_PAGE_BYTES |
Maximum bytes per HTML page | 2097152 (2MB) |
BRANDING_CRAWL_MAX_ASSET_BYTES |
Maximum bytes per CSS/asset file | 5242880 (5MB) |
BRANDING_CRAWL_REQUEST_DELAY_MS |
Delay between requests (rate limiting) | 150 |
| Variable | Purpose | Default |
|---|---|---|
LOGO_ANALYSIS_COUNT |
Number of top candidates to analyze | 8 |
LOGO_ANALYSIS_TIMEOUT_MS |
Timeout for image analysis (ms) | 12000 |
LOGO_DOWNLOAD_TIMEOUT_MS |
Timeout for logo download (ms) | 15000 |
az group create --name glovedesign-rg --location eastus
az deployment group create \
--resource-group glovedesign-rg \
--template-file infra/main.bicep \
--parameters location=eastus projectName=glovedesignThis repo includes .github/workflows/deploy-infra.yml. It uses Azure OIDC (no long-lived secrets).
- Create a service principal with access to your resource group (Contributor is enough).
- Add a federated credential for your GitHub repo/branch.
- Add GitHub repository secrets:
AZURE_CLIENT_IDAZURE_TENANT_IDAZURE_SUBSCRIPTION_ID
- Add GitHub repository variables (optional defaults are already in the workflow):
AZURE_RESOURCE_GROUP(example:glovedesign-rg)AZURE_LOCATION(example:eastus)PROJECT_NAME(example:glovedesign)WIZARD_IMAGE(example:ghcr.io/your-org/glove-wizard:latest)
- Run the workflow manually (this repo does not auto-deploy infra on push).
This repo includes .github/workflows/deploy-functions.yml for the Function App.
- Add GitHub repository secrets:
AZURE_CLIENT_IDAZURE_TENANT_IDAZURE_SUBSCRIPTION_ID
- Add GitHub repository variables:
FUNCTION_APP_NAME(example:gloveapp-b4f0gvapbye9eda2)AZURE_RESOURCE_GROUP(example:glovedesign-rg)
- Run the workflow manually.
Deploy the Function App using your preferred CI/CD (GitHub Actions or func azure functionapp publish).
Build and deploy the wizard worker container to Azure Container Apps Job or an HTTP-enabled container app and set WIZARD_ENDPOINT or WIZARD_QUEUE accordingly.
curl -X POST https://<function-app>.azurewebsites.net/api/jobs \
-H "x-functions-key: <key>" \
-H "content-type: application/json" \
-d '{"teamUrl":"https://arlingtontravelbaseball.org/","mode":"proposal"}'curl -X GET https://<function-app>.azurewebsites.net/api/jobs/<jobId> \
-H "x-functions-key: <key>"{
"jobId": "uuid",
"teamUrl": "https://example.com",
"mode": "proposal",
"stage": "completed",
"status": "Succeeded",
"outputs": {
"logo": { "path": "jobs/<jobId>/logo.png", "url": "https://..." },
"palette": { "path": "jobs/<jobId>/palette.json", "url": "https://..." },
"design": { "path": "jobs/<jobId>/glove_design.json", "url": "https://..." },
"proposal": { "path": "jobs/<jobId>/proposal.md", "url": "https://..." },
"crawlReport": { "path": "jobs/<jobId>/crawl_report.json", "url": "https://..." },
"wizardSchema": { "path": "jobs/<jobId>/wizard_schema_snapshot.json", "url": "https://..." }
},
"autofillAttempted": false,
"autofillSucceeded": false
}/jobs/{jobId}/logo.(png|jpg|svg)/jobs/{jobId}/palette.json/jobs/{jobId}/glove_design.json/jobs/{jobId}/proposal.md/jobs/{jobId}/crawl_report.json/jobs/{jobId}/wizard_schema_snapshot.json(autofill attempted)/jobs/{jobId}/configured.png(autofill success only)
- Input validation + SSRF mitigation (http/https only, block private IPs, DNS rebinding checks, redirect caps).
- Robots.txt best-effort compliance.
- Service Bus, Blob Storage, Cosmos DB accessed via Managed Identity when deployed.
- Durable Functions activity retries should be configured for transient failures.
- Allow/deny lists can be enforced at the API layer before enqueuing jobs.
- Proposal-first: pipeline produces a proposal without Playwright dependency.
- Auditability: crawl report and scoring reasons stored per job.
- Reliability: bounded crawl caps, timeouts, and dedicated worker for autofill.
npm test- Job stays in
Running- Check Service Bus queue length and dead-letter messages.
- Verify the Function App has access to Service Bus and Storage.
- No artifacts in Blob Storage
- Confirm
BLOB_URLandBLOB_CONTAINER. - Ensure Function App identity has
Storage Blob Data Contributor.
- Confirm
- Wizard autofill never runs
- If using HTTP: verify
WIZARD_ENDPOINTis reachable. - If using queues: verify
WIZARD_QUEUEandWIZARD_RESULTS_QUEUE.
- If using HTTP: verify
- Wizard autofill fails immediately
- Site may be blocked or require a login/captcha (this is expected; proposal-only still works).
Note: This README is updated regularly during deployment troubleshooting.
Last UI redeploy trigger: 2026-01-28