Switch to orders.seahaven.com, add roster dropdown, fix deadline (#9)
Some checks are pending
Deploy / deploy (push) Waiting to run

- Change custom domain from orders.seahavenind.com to
  orders.seahaven.com to match other subdomain conventions
- Add GET /api/roster endpoint returning employee names/emails
- Replace name/email text inputs with dropdown populated from
  roster API (falls back to embedded roster for local dev)
- Fix order deadline text from Wednesday to Thursday 11:59 PM
- Fix Slack API calls: use form-urlencoded for conversations and
  users methods that reject JSON body encoding
This commit is contained in:
Adam Moussa 2026-05-12 20:16:46 -04:00 • committed by GitHub
parent 11ea599bbd
commit a8adbdc116
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 57 additions and 10 deletions

View file

@ -32,7 +32,7 @@ who haven't ordered
| Monday 6:55am ET | Sync employee roster from Slack channel membership | EventBridge → Lambda → DynamoDB |
| Monday 7am ET | Email previous week's payroll deductions to `payroll@` | EventBridge → Lambda → SES |
| Monday 8am ET | Scrape menu, generate form, upload to S3, post link to Slack | GitHub Actions cron |
| Mon–Thu | Employees visit `orders.seahavenind.com` and submit orders | S3 static form → API Gateway → Lambda → DynamoDB |
| Mon–Thu | Employees visit `orders.seahaven.com` and submit orders | S3 static form → API Gateway → Lambda → DynamoDB |
| Thursday 10am ET | DM employees who haven't ordered yet | EventBridge → Lambda → Slack DM |
| Thursday 6pm ET | Close form, aggregate orders, post Redefine order summary to Slack | EventBridge → Lambda chain |
@ -41,7 +41,7 @@ who haven't ordered
Stack name: `meal-order-manager` (us-east-1)
- **S3** — `meal-order-manager-form-*` (static form hosting), `meal-order-manager-reports-*` (CSV reports)
- **CloudFront** — HTTPS distribution with custom domain `orders.seahavenind.com`
- **CloudFront** — HTTPS distribution with custom domain `orders.seahaven.com`
- **DynamoDB** — `meal-order-manager-orders` (orders, menu, roster, config)
- **API Gateway** — HttpApi for order submission
- **Lambda** — 6 functions: submit-order, close-form, aggregate-orders, slack-notifier, sync-roster, email-report
@ -75,7 +75,7 @@ sam deploy
2. Create the form API key secret: `aws secretsmanager create-secret --name meal-order-manager/form-api-key --secret-string "$(openssl rand -hex 32)"`
3. Update the Slack channel SSM parameter: `aws ssm put-parameter --name /meal-order-manager/slack-channel-id --value "C0XXXXXXX" --overwrite`
4. Verify SES sender identity for `adam@seahavenind.com`
5. Set up DNS: CNAME `orders.seahavenind.com` → CloudFront distribution domain
5. Set up DNS: CNAME `orders.seahaven.com` → CloudFront distribution domain
6. Roster syncs automatically from Slack channel members (runs Monday 6:55am ET), or seed manually: `python3 scripts/seed_roster.py`
## Local Workflow (no AWS)

View file

@ -3,7 +3,7 @@ import os
from datetime import datetime
from zoneinfo import ZoneInfo
from shared.db import current_week, get_form_status, put_order
from shared.db import current_week, get_form_status, get_roster, put_order
from shared.secrets import get_secret
EASTERN = ZoneInfo("America/New_York")
@ -24,6 +24,9 @@ def lambda_handler(event, context):
if "/form-status/" in path:
return handle_form_status(event)
if "/roster" in path:
return handle_roster()
if method == "POST":
return handle_submit(event)
@ -36,6 +39,12 @@ def handle_form_status(event):
return response(200, {"week": week, "status": status})
def handle_roster():
roster = get_roster()
employees = [{"name": e["name"], "email": e["email"]} for e in roster]
return response(200, {"employees": employees})
def handle_submit(event):
api_key = event.get("headers", {}).get("x-api-key", "")
if api_key != _get_api_key():

View file

@ -7,4 +7,4 @@ s3_prefix = "meal-order-manager"
region = "us-east-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "CustomDomain=orders.seahavenind.com CertificateArn=arn:aws:acm:us-east-1:328440206208:certificate/CHANGE-ME PayrollEmail=payroll@seahavenind.com SenderEmail=adam@seahavenind.com"
parameter_overrides = "CustomDomain=orders.seahaven.com CertificateArn=arn:aws:acm:us-east-1:328440206208:certificate/CHANGE-ME PayrollEmail=payroll@seahavenind.com SenderEmail=adam@seahavenind.com"

View file

@ -39,11 +39,12 @@ def generate_form(
) -> str:
meals_json = json.dumps(menu["meals"])
roster_json = json.dumps(config.get("roster", []))
deadline = config.get("order_deadline", "Thursday 6:00 PM")
deadline = config.get("order_deadline", "Thursday 11:59 PM")
week = datetime.now().strftime("%Y-W%U")
scraped_at = menu.get("scraped_at", "unknown")
submit_url = f"{api_url}/api/submit-order" if api_url else "/api/submit-order"
status_url = f"{api_url}/api/form-status/{week}" if api_url else ""
roster_url = f"{api_url}/api/roster" if api_url else "/api/roster"
api_key_json = json.dumps(api_key)
return f"""<!DOCTYPE html>
@ -106,9 +107,8 @@ body {{ padding-bottom: 80px; }}
<div class="employee-info">
<label for="emp-name">Your Name</label>
<input type="text" id="emp-name" placeholder="Full name" autocomplete="name">
<label for="emp-email">Your Email</label>
<input type="email" id="emp-email" placeholder="you@seahavenind.com" autocomplete="email">
<select id="emp-name"><option value="">Loading...</option></select>
<input type="hidden" id="emp-email">
</div>
<input type="text" class="search-bar" id="search" placeholder="Search meals...">
@ -138,6 +138,7 @@ const MEALS = {meals_json};
const ROSTER = {roster_json};
const SUBMIT_URL = '{submit_url}';
const STATUS_URL = '{status_url}';
const ROSTER_URL = '{roster_url}';
const API_KEY = {api_key_json};
const WEEK = '{week}';
const quantities = {{}};
@ -145,6 +146,7 @@ let formClosed = false;
function init() {{
checkFormStatus();
loadRoster();
// Build filter buttons
const tags = new Set();
MEALS.forEach(m => (m.dietary_tags || []).forEach(t => tags.add(t)));
@ -270,6 +272,36 @@ async function submitOrder() {{
}}
}}
async function loadRoster() {{
try {{
const res = await fetch(ROSTER_URL);
const data = await res.json();
const select = document.getElementById('emp-name');
select.innerHTML = '<option value="">Select your name</option>';
(data.employees || ROSTER).forEach(emp => {{
const opt = document.createElement('option');
opt.value = emp.name;
opt.dataset.email = emp.email;
opt.textContent = emp.name;
select.appendChild(opt);
}});
select.addEventListener('change', () => {{
const selected = select.options[select.selectedIndex];
document.getElementById('emp-email').value = selected?.dataset?.email || '';
}});
}} catch (e) {{
const select = document.getElementById('emp-name');
select.innerHTML = '<option value="">Select your name</option>';
ROSTER.forEach(emp => {{
const opt = document.createElement('option');
opt.value = emp.name;
opt.dataset.email = emp.email;
opt.textContent = emp.name;
select.appendChild(opt);
}});
}}
}}
async function checkFormStatus() {{
if (!STATUS_URL) return;
try {{

View file

@ -7,7 +7,7 @@ Description: >
Parameters:
CustomDomain:
Type: String
Default: orders.seahavenind.com
Default: orders.seahaven.com
Description: Custom domain for the order form (requires ACM cert)
CertificateArn:
Type: String
@ -250,6 +250,12 @@ Resources:
ApiId: !Ref OrderApi
Path: /api/form-status/{week}
Method: GET
Roster:
Type: HttpApi
Properties:
ApiId: !Ref OrderApi
Path: /api/roster
Method: GET
CloseFormFunction:
Type: AWS::Serverless::Function