From 582e571eff037f8d82ec38e3d5dec9bdd9dfb9c4 Mon Sep 17 00:00:00 2001 From: avi Date: Mon, 28 Sep 2026 15:09:49 -0500 Subject: [PATCH 1/5] Replace BTCPay payments with free pre-orders + live ready-status - server.js: order store with K484-XXXX codes, stand assignment, SSE status to customers, passcode-gated staff API, persists to .data/ - staff.html at /staff: order board with Mark ready / Collected / Cancel - index.html: drop BTCPay API key, invoices, QR, BTC price; orders now server-backed with live PREPARING -> READY updates and stand location - e2e-test.js rewritten for the order loop (14 checks, all passing) - README added; .gitignore covers server/.data --- .gitignore | 3 + README.md | 34 ++ index.html | 394 ++++++-------------- server/.env.example | 21 +- server/docker-compose.btcpay.yml | 188 ---------- server/e2e-test.js | 205 ++++++----- server/mock-btcpay.js | 124 ------- server/package.json | 7 +- server/server.js | 607 +++++++++++-------------------- server/staff.html | 189 ++++++++++ 10 files changed, 666 insertions(+), 1106 deletions(-) create mode 100644 README.md delete mode 100644 server/docker-compose.btcpay.yml delete mode 100644 server/mock-btcpay.js create mode 100644 server/staff.html diff --git a/.gitignore b/.gitignore index 513af21..a4e3b96 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ target/ *.jks tsconfig.tsbuildinfo instance/ + +server/.data/ +server/.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..268cc0d --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# KITCHEN 484 · Solarpunk Summit + +Festival kitchen site: browse the food schedule, place a free pre-order, +and watch it flip to **READY** the moment the kitchen fires it — with the +stand to pick it up from. + +## What it is + +- `index.html` — customer PWA (schedule, cart, pickup codes, live status) +- `server/server.js` — zero-dependency Node backend: order store, pickup + codes (`K484-XXXX`), SSE live status, static hosting +- `server/staff.html` — staff order board at `/staff` (passcode-gated: + mark orders READY / COLLECTED / CANCELLED, filter by stand) +- `home.html` — K484 association page (separate, untouched) + +Payments were removed (Sep 2026): pre-orders are free reservations; +people pay at the stand. The old BTCPay proxy lives in git history +(commit `601c85f`) if it's ever needed again. + +## Run + +```sh +cd server +cp .env.example .env # set STAFF_PASSCODE (openssl rand -hex 4) +node server.js # http://localhost:8787 +``` + +Orders persist to `server/.data/orders.json` (gitignored). + +## Test + +```sh +cd server && npm run test:e2e # 14 checks: order loop, auth, SSE, restart persistence +``` diff --git a/index.html b/index.html index df173c1..4d6a3bd 100644 --- a/index.html +++ b/index.html @@ -14,7 +14,6 @@ - + + +
+
+

KITCHEN 484 · STAFF

+
pre-order board — tap READY when the food is up
+
+
+
+
+ + +
+ + + From c9f8df996eb2257d7d5ead6624e57e27a90a8fe1 Mon Sep 17 00:00:00 2001 From: avi Date: Mon, 28 Sep 2026 15:23:19 -0500 Subject: [PATCH 2/5] README: phone access (LAN URL, tunnel fallback, QR) --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index 268cc0d..33ad019 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,27 @@ Orders persist to `server/.data/orders.json` (gitignored). ```sh cd server && npm run test:e2e # 14 checks: order loop, auth, SSE, restart persistence ``` + +## Open it on your phone + +Same Wi-Fi (if the network allows device-to-device): + +``` +http://:8787 # laptop IP from: ip -4 addr | grep 10. +http://:8787/staff # staff board (passcode from server/.env) +``` + +If the phone can't connect (some Wi-Fi networks isolate devices — this +happens on ours), use a tunnel instead: + +```sh +~/.local/bin/cloudflared tunnel --url http://localhost:8787 +# prints https://.trycloudflare.com — open that on the phone +# (URL changes on every tunnel restart) +``` + +Typing the long URL fails often (browser eats it as a search). QR to the rescue: + +```sh +qrencode -s 12 -m 2 -o /tmp/k484-qr.png "https://" # then scan it +``` From a4b56a8dc924a520645b5489e4aa09a29a0ff2c4 Mon Sep 17 00:00:00 2001 From: avi Date: Mon, 28 Sep 2026 15:31:38 -0500 Subject: [PATCH 3/5] Add placeholder pickup locations per stand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit STAND_LOCATIONS map in server.js keyed by menu item name; every order now carries a 'where to find it' line (PLACEHOLDER — edit when the site map is final). Staff can override per order when marking ready. --- server/server.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/server/server.js b/server/server.js index 575a5b0..4f4259e 100644 --- a/server/server.js +++ b/server/server.js @@ -24,6 +24,28 @@ const STAFF_PASSCODE = process.env['ST'+'AFF_PASSCODE'] || ''; // Each menu item is its own stand; mixed orders consolidate at Kitchen 484. const DEFAULT_STAND = 'Kitchen 484'; const DEFAULT_LOCATION = 'Center of camp, by the big solar dish'; + +// PLACEHOLDER stand locations — edit these when the site map is final. +// Keyed by menu item name; orders for that item show this text wherever +// the app says where to pick up (success screen, orders view, staff board). +const STAND_LOCATIONS = { + 'Coffee Station': 'PLACEHOLDER — east path, next to the water station', + 'Sweets Cart': 'PLACEHOLDER — north gate, under the yellow canopy', + 'Taco Stand': 'PLACEHOLDER — main lawn, food row #1', + 'BBQ Pit': 'PLACEHOLDER — downwind corner behind the wood pile', + 'Chicken Grill': 'PLACEHOLDER — main lawn, food row #2', + 'Pasta Bar': 'PLACEHOLDER — west tent, near the stage', + 'Drinks': 'PLACEHOLDER — center of camp, big cooler', + 'Garden Burgers': 'PLACEHOLDER — main lawn, food row #2', + 'Grill 484': 'PLACEHOLDER — center of camp, by the big solar dish', + 'Ramen Tent': 'PLACEHOLDER — south tent, steam visible from path', + 'Flatbread Oven': 'PLACEHOLDER — east tent, smell the bread', + 'Dumpling Bar': 'PLACEHOLDER — west tent, next to Pasta Bar', + 'Farewell Feast · BBQ': 'PLACEHOLDER — main lawn, long tables', +}; +function locationFor(stand) { + return STAND_LOCATIONS[stand] || STAND_LOCATIONS[DEFAULT_STAND] || DEFAULT_LOCATION; +} // --- order store --------------------------------------------------------- fs.mkdirSync(DATA_DIR, { recursive: true }); let orders = []; @@ -132,7 +154,7 @@ async function handle(req, res) { const ids = Object.keys(items); if (!ids.length) return json(res, 400, { error: 'cart is empty' }); const stand = String(b.stand || DEFAULT_STAND).trim().slice(0, 40) || DEFAULT_STAND; - const standLocation = String(b.standLocation || DEFAULT_LOCATION).trim().slice(0, 80); + const standLocation = String(b.standLocation || locationFor(stand)).trim().slice(0, 80); const order = { code: makeCode(), status: 'PREPARING', From f91c29c0351107ebbb05593c3ca63e8f3d44af5f Mon Sep 17 00:00:00 2001 From: avi Date: Mon, 28 Sep 2026 15:37:11 -0500 Subject: [PATCH 4/5] Remove Coffee Station and Drinks from schedule; drop all prices Menu is food stands only. Cart, checkout, and order cards no longer show or compute dollar amounts (free pre-orders). e2e still 14/14. --- index.html | 76 +++++++++++++++++++++--------------------------------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/index.html b/index.html index 4d6a3bd..b62c109 100644 --- a/index.html +++ b/index.html @@ -102,7 +102,6 @@ header{position:sticky;top:0;z-index:40;background:rgb(255 255 255/.85);backdrop .item-emoji{width:46px;height:46px;flex:0 0 46px;border-radius:14px;background:var(--bg2);border:1px solid var(--line);display:grid;place-items:center;font-size:24px} .item-name{font-size:15px;font-weight:800;display:flex;align-items:center;gap:8px;flex-wrap:wrap} .item-desc{font-size:12px;color:var(--ink2);margin-top:2px} -.item-price{margin-left:auto;font-family:var(--display);font-weight:900;font-size:16px;white-space:nowrap} .item-foot{display:flex;align-items:center;gap:8px;margin-top:12px;flex-wrap:wrap} .status-pill{font-size:10px;font-weight:800;letter-spacing:.8px;text-transform:uppercase;padding:5px 10px;border-radius:999px;display:inline-flex;align-items:center;gap:6px} .status-pill .dot{width:6px;height:6px;border-radius:99px;background:currentColor} @@ -300,46 +299,36 @@ function dayLabel(offset){ const FEST={ days:[ { tag:'DAY 1', items:[ - {id:'d1-coffee', name:'Coffee Station', emoji:'☕', price:4, start:540, end:1080, tags:['VG'], desc:'Slow-drip single origin + oat milk latte, served from the wood-fired cart.'}, - {id:'d1-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1320, tags:['V','GF'], desc:'Fresh fruit skewers, sorbet cups, and festival brownies.'}, - {id:'d1-taco', name:'Taco Stand', emoji:'🌮', price:8, start:720, end:870, tags:['GF'], desc:'Hand-pressed corn tortillas, slow-braised grass-fed filling, house salsa.'}, - {id:'d1-bbq', name:'BBQ Pit', emoji:'🍖', price:12, start:780, end:930, tags:[], desc:'12-hour offset-smoked brisket & ribs. No seed oils — rendered tallow only.'}, - {id:'d1-chicken',name:'Chicken Grill', emoji:'🍗', price:9, start:1050, end:1230, tags:['GF'], desc:'Buttermilk fried chicken (tallow-fried) with slaw.'}, - {id:'d1-pasta', name:'Pasta Bar', emoji:'🍝', price:10, start:1080, end:1200, tags:['V'], desc:'Fresh-cut daily pasta, garden vegetables, house olive oil.'}, - {id:'d1-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1320, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'} - ]}, + {id:'d1-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1320, tags:['V','GF'], desc:'Fresh fruit skewers, sorbet cups, and festival brownies.'}, + {id:'d1-taco', name:'Taco Stand', emoji:'🌮',start:720, end:870, tags:['GF'], desc:'Hand-pressed corn tortillas, slow-braised grass-fed filling, house salsa.'}, + {id:'d1-bbq', name:'BBQ Pit', emoji:'🍖',start:780, end:930, tags:[], desc:'12-hour offset-smoked brisket & ribs. No seed oils — rendered tallow only.'}, + {id:'d1-chicken',name:'Chicken Grill', emoji:'🍗',start:1050, end:1230, tags:['GF'], desc:'Buttermilk fried chicken (tallow-fried) with slaw.'}, + {id:'d1-pasta', name:'Pasta Bar', emoji:'🍝',start:1080, end:1200, tags:['V'], desc:'Fresh-cut daily pasta, garden vegetables, house olive oil.'} + ]},, { tag:'DAY 2', items:[ - {id:'d2-coffee', name:'Coffee Station', emoji:'☕', price:4, start:570, end:1020, tags:['VG'], desc:'Slow-drip single origin + oat milk latte.'}, - {id:'d2-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1260, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'}, - {id:'d2-burger', name:'Garden Burgers', emoji:'🍔', price:11, start:720, end:900, tags:[], desc:'Grass-fed beef patty, brioche, charred onion, herb aioli.'}, - {id:'d2-grill', name:'Grill 484', emoji:'🔥', price:9, start:750, end:960, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'}, - {id:'d2-ramen', name:'Ramen Tent', emoji:'🍜', price:12, start:1020, end:1200, tags:['V'], desc:'Miso broth simmered all day, soft egg, scallion, pickled ginger.'}, - {id:'d2-drinks', name:'Drinks', emoji:'🥤', price:3, start:570, end:1290, tags:['VG'], desc:'Cold brew, kombucha, herbal tea, lemonade.'} - ]}, + {id:'d2-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1260, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'}, + {id:'d2-burger', name:'Garden Burgers', emoji:'🍔',start:720, end:900, tags:[], desc:'Grass-fed beef patty, brioche, charred onion, herb aioli.'}, + {id:'d2-grill', name:'Grill 484', emoji:'🔥',start:750, end:960, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'}, + {id:'d2-ramen', name:'Ramen Tent', emoji:'🍜',start:1020, end:1200, tags:['V'], desc:'Miso broth simmered all day, soft egg, scallion, pickled ginger.'} + ]},, { tag:'DAY 3', items:[ - {id:'d3-coffee', name:'Coffee Station', emoji:'☕', price:4, start:540, end:1050, tags:['VG'], desc:'Slow-drip single origin, Saturday long pour.'}, - {id:'d3-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1320, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, Saturday brownie batch.'}, - {id:'d3-flatbread',name:'Flatbread Oven', emoji:'🫓', price:9, start:720, end:930, tags:['V','GF'], desc:'Wood-fired flatbreads — charred vegetables, whipped feta, house olive oil.'}, - {id:'d3-bbq', name:'BBQ Pit', emoji:'🍖', price:12, start:780, end:960, tags:[], desc:'Offset-smoked brisket & ribs. The weekend is for the pit.'}, - {id:'d3-dumplings',name:'Dumpling Bar', emoji:'🥟', price:10, start:1020, end:1230, tags:['GF'], desc:'Pan-seared vegetable & chicken dumplings, chili crisp.'}, - {id:'d3-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1320, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'} - ]}, + {id:'d3-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1320, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, Saturday brownie batch.'}, + {id:'d3-flatbread',name:'Flatbread Oven', emoji:'🫓',start:720, end:930, tags:['V','GF'], desc:'Wood-fired flatbreads — charred vegetables, whipped feta, house olive oil.'}, + {id:'d3-bbq', name:'BBQ Pit', emoji:'🍖',start:780, end:960, tags:[], desc:'Offset-smoked brisket & ribs. The weekend is for the pit.'}, + {id:'d3-dumplings',name:'Dumpling Bar', emoji:'🥟',start:1020, end:1230, tags:['GF'], desc:'Pan-seared vegetable & chicken dumplings, chili crisp.'} + ]},, { tag:'DAY 4', items:[ - {id:'d4-coffee', name:'Coffee Station', emoji:'☕', price:4, start:540, end:1020, tags:['VG'], desc:'Slow-drip single origin + oat milk latte.'}, - {id:'d4-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1290, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'}, - {id:'d4-taco', name:'Taco Stand', emoji:'🌮', price:8, start:720, end:870, tags:['GF'], desc:'Corn tortillas, slow-braised grass-fed filling, house salsa.'}, - {id:'d4-pasta', name:'Pasta Bar', emoji:'🍝', price:10, start:900, end:1140, tags:['V'], desc:'Sunday pasta — fresh-cut, garden vegetables, house olive oil.'}, - {id:'d4-grill', name:'Grill 484', emoji:'🔥', price:9, start:960, end:1170, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'}, - {id:'d4-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1290, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'} - ]}, + {id:'d4-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1290, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'}, + {id:'d4-taco', name:'Taco Stand', emoji:'🌮',start:720, end:870, tags:['GF'], desc:'Corn tortillas, slow-braised grass-fed filling, house salsa.'}, + {id:'d4-pasta', name:'Pasta Bar', emoji:'🍝',start:900, end:1140, tags:['V'], desc:'Sunday pasta — fresh-cut, garden vegetables, house olive oil.'}, + {id:'d4-grill', name:'Grill 484', emoji:'🔥',start:960, end:1170, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'} + ]},, { tag:'FINAL DAY', items:[ - {id:'d5-coffee', name:'Coffee Station', emoji:'☕', price:4, start:600, end:960, tags:['VG'], desc:'One last pour. Single origin, slow drip.'}, - {id:'d5-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1200, tags:['V','GF'], desc:'Sorbet, fruit, brownies — farewell batch.'}, - {id:'d5-taco', name:'Taco Stand', emoji:'🌮', price:8, start:720, end:840, tags:['GF'], desc:'Corn tortillas, slow-braised filling.'}, - {id:'d5-feast', name:'Farewell Feast · BBQ', emoji:'🍖', price:14, start:720, end:960, tags:[], desc:'The full spread: brisket, ribs, tallow fries, slaw, bread.'}, - {id:'d5-pasta', name:'Pasta Bar', emoji:'🍝', price:10, start:1020, end:1170, tags:['V'], desc:'Fresh pasta, garden vegetables, olive oil.'}, - {id:'d5-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1230, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'} - ]} + {id:'d5-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1200, tags:['V','GF'], desc:'Sorbet, fruit, brownies — farewell batch.'}, + {id:'d5-taco', name:'Taco Stand', emoji:'🌮',start:720, end:840, tags:['GF'], desc:'Corn tortillas, slow-braised filling.'}, + {id:'d5-feast', name:'Farewell Feast · BBQ', emoji:'🍖',start:720, end:960, tags:[], desc:'The full spread: brisket, ribs, tallow fries, slaw, bread.'}, + {id:'d5-pasta', name:'Pasta Bar', emoji:'🍝',start:1020, end:1170, tags:['V'], desc:'Fresh pasta, garden vegetables, olive oil.'} + ]}, ] }; FEST.days.forEach((d,i)=>{ const L=dayLabel(i); d.name=L.name; d.date=L.date; d.iso=L.iso; }); @@ -363,7 +352,6 @@ const fmtT=min=>{let h=Math.floor(min/60),m=min%60,ap=h>=12?'PM':'AM';h=h%12||12 const fmtDur=min=>{const h=Math.floor(min/60),m=Math.round(min%60);return h? (h+'h'+(m?' '+m+'m':'')) : m+'m';}; const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); function toast(msg){const t=$('#toast');t.textContent=msg;t.classList.add('show');clearTimeout(t._x);t._x=setTimeout(()=>t.classList.remove('show'),2200);} -function cartTotal(){let t=0;for(const id in state.cart){const it=findItem(id);if(it)t+=it.price*state.cart[id];}return t;} function cartCount(){return Object.values(state.cart).reduce((a,b)=>a+b,0);} function findItem(id){for(const d of FEST.days)for(const it of d.items)if(it.id===id)return it;return null;} function findDayOf(id){for(let i=0;ix.id===id))return i;} @@ -407,7 +395,6 @@ function renderTimeline(){ '
'+item.emoji+'
'+ '
'+item.name+' '+item.tags.map(t=>''+t+'').join('')+'
'+ '
'+item.desc+'
'+ - '
'+fmt$(item.price)+'
'+ ''+ '
'+ ''+st.label+''+ @@ -479,12 +466,10 @@ function stepCart(){ '
'+it.emoji+'
'+ '
'+it.name+'
'+fmtT(it.start)+' – '+fmtT(it.end)+'
'+ '
'+state.cart[id]+'
'+ - '
'+fmt$(it.price*state.cart[id])+'
'; + ''; }).join(''); - const total=cartTotal(); return '

YOUR ORDER

SOLARPUNK SUMMIT · pickup at the stand window
'+ (rows||'
🧺
Nothing here yet — add something from the schedule.
')+ - '
Total'+fmt$(total)+'
'+ '
Free pre-order — pay at the stand when you pick up.
'+ ''; } @@ -502,7 +487,7 @@ function stepCheckout(){ '
'+ '
'+ '
'+ - '
Total · '+cartCount()+' items'+fmt$(cartTotal())+'
'+ + '
Total'+cartCount()+' items
'+ ''+ ''+ ''; @@ -552,7 +537,6 @@ function afterCartChange(){saveCart();renderFab();renderTimeline();renderSheet() /* ============================== ORDERS (server-backed) ============================== */ async function placeOrder(info){ - const usd=cartTotal(); const day=FEST.days.find(d=>d.items.some(it=>info.window&&Math.abs(it.start-info.window[0])<1&&Math.abs(it.end-info.window[1])<1))||FEST.days[state.dayIdx]; const pickupLabel=fmtT(info.window[0])+' \u2013 '+fmtT(info.window[1]); const ids=Object.keys(state.cart); @@ -565,7 +549,7 @@ async function placeOrder(info){ body:JSON.stringify({ name:info.name,notes:info.notes,items:state.cart, window:info.window,pickupLabel,itemsSummary, - stand,totalUsd:usd, + stand, day:day.name+' '+day.date }) }); @@ -644,7 +628,7 @@ function renderOrders(){ '
'+esc(o.code)+''+badge+'
'+ '
'+esc(o.day||'')+' \u00b7 pickup '+esc(o.pickupLabel||'')+' \u00b7 '+esc(o.name||'')+(o.notes?' \u00b7 \u201c'+esc(o.notes)+'\u201d':'')+'
'+ '
'+esc(o.itemsSummary||'')+'
'+ - '
'+fmt$(o.totalUsd||0)+''+esc(o.stand||'')+'
'+ + '
'+esc(o.stand||'')+'
'+ readyBanner+ (o.status!=='COLLECTED'&&o.status!=='CANCELLED'?'
':'')+ ''; From 006eb23356995c67444ca813b977c702d497dba8 Mon Sep 17 00:00:00 2001 From: avi Date: Mon, 28 Sep 2026 15:50:19 -0500 Subject: [PATCH 5/5] Remove Sweets Cart; show placeholder stand location on every schedule card --- index.html | 25 ++++++++++++++++++------- server/server.js | 1 - 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/index.html b/index.html index b62c109..344fad1 100644 --- a/index.html +++ b/index.html @@ -102,6 +102,7 @@ header{position:sticky;top:0;z-index:40;background:rgb(255 255 255/.85);backdrop .item-emoji{width:46px;height:46px;flex:0 0 46px;border-radius:14px;background:var(--bg2);border:1px solid var(--line);display:grid;place-items:center;font-size:24px} .item-name{font-size:15px;font-weight:800;display:flex;align-items:center;gap:8px;flex-wrap:wrap} .item-desc{font-size:12px;color:var(--ink2);margin-top:2px} +.item-loc{font-size:11px;font-weight:700;color:var(--amber);margin-top:3px;letter-spacing:.02em} .item-foot{display:flex;align-items:center;gap:8px;margin-top:12px;flex-wrap:wrap} .status-pill{font-size:10px;font-weight:800;letter-spacing:.8px;text-transform:uppercase;padding:5px 10px;border-radius:999px;display:inline-flex;align-items:center;gap:6px} .status-pill .dot{width:6px;height:6px;border-radius:99px;background:currentColor} @@ -296,39 +297,48 @@ function dayLabel(offset){ iso:d.toISOString().slice(0,10) }; } +// PLACEHOLDER stand locations — keep in sync with server.js STAND_LOCATIONS. +const STAND_LOCATIONS={ + 'Taco Stand': 'PLACEHOLDER — main lawn, food row #1', + 'BBQ Pit': 'PLACEHOLDER — downwind corner behind the wood pile', + 'Chicken Grill': 'PLACEHOLDER — main lawn, food row #2', + 'Pasta Bar': 'PLACEHOLDER — west tent, near the stage', + 'Garden Burgers': 'PLACEHOLDER — main lawn, food row #2', + 'Grill 484': 'PLACEHOLDER — center of camp, by the big solar dish', + 'Ramen Tent': 'PLACEHOLDER — south tent, steam visible from path', + 'Flatbread Oven': 'PLACEHOLDER — east tent, smell the bread', + 'Dumpling Bar': 'PLACEHOLDER — west tent, next to Pasta Bar', + 'Farewell Feast · BBQ': 'PLACEHOLDER — main lawn, long tables', +}; + const FEST={ days:[ { tag:'DAY 1', items:[ - {id:'d1-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1320, tags:['V','GF'], desc:'Fresh fruit skewers, sorbet cups, and festival brownies.'}, {id:'d1-taco', name:'Taco Stand', emoji:'🌮',start:720, end:870, tags:['GF'], desc:'Hand-pressed corn tortillas, slow-braised grass-fed filling, house salsa.'}, {id:'d1-bbq', name:'BBQ Pit', emoji:'🍖',start:780, end:930, tags:[], desc:'12-hour offset-smoked brisket & ribs. No seed oils — rendered tallow only.'}, {id:'d1-chicken',name:'Chicken Grill', emoji:'🍗',start:1050, end:1230, tags:['GF'], desc:'Buttermilk fried chicken (tallow-fried) with slaw.'}, {id:'d1-pasta', name:'Pasta Bar', emoji:'🍝',start:1080, end:1200, tags:['V'], desc:'Fresh-cut daily pasta, garden vegetables, house olive oil.'} ]},, { tag:'DAY 2', items:[ - {id:'d2-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1260, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'}, {id:'d2-burger', name:'Garden Burgers', emoji:'🍔',start:720, end:900, tags:[], desc:'Grass-fed beef patty, brioche, charred onion, herb aioli.'}, {id:'d2-grill', name:'Grill 484', emoji:'🔥',start:750, end:960, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'}, {id:'d2-ramen', name:'Ramen Tent', emoji:'🍜',start:1020, end:1200, tags:['V'], desc:'Miso broth simmered all day, soft egg, scallion, pickled ginger.'} ]},, { tag:'DAY 3', items:[ - {id:'d3-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1320, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, Saturday brownie batch.'}, {id:'d3-flatbread',name:'Flatbread Oven', emoji:'🫓',start:720, end:930, tags:['V','GF'], desc:'Wood-fired flatbreads — charred vegetables, whipped feta, house olive oil.'}, {id:'d3-bbq', name:'BBQ Pit', emoji:'🍖',start:780, end:960, tags:[], desc:'Offset-smoked brisket & ribs. The weekend is for the pit.'}, {id:'d3-dumplings',name:'Dumpling Bar', emoji:'🥟',start:1020, end:1230, tags:['GF'], desc:'Pan-seared vegetable & chicken dumplings, chili crisp.'} ]},, { tag:'DAY 4', items:[ - {id:'d4-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1290, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'}, {id:'d4-taco', name:'Taco Stand', emoji:'🌮',start:720, end:870, tags:['GF'], desc:'Corn tortillas, slow-braised grass-fed filling, house salsa.'}, {id:'d4-pasta', name:'Pasta Bar', emoji:'🍝',start:900, end:1140, tags:['V'], desc:'Sunday pasta — fresh-cut, garden vegetables, house olive oil.'}, {id:'d4-grill', name:'Grill 484', emoji:'🔥',start:960, end:1170, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'} ]},, { tag:'FINAL DAY', items:[ - {id:'d5-sweets', name:'Sweets Cart', emoji:'🍭',start:660, end:1200, tags:['V','GF'], desc:'Sorbet, fruit, brownies — farewell batch.'}, {id:'d5-taco', name:'Taco Stand', emoji:'🌮',start:720, end:840, tags:['GF'], desc:'Corn tortillas, slow-braised filling.'}, {id:'d5-feast', name:'Farewell Feast · BBQ', emoji:'🍖',start:720, end:960, tags:[], desc:'The full spread: brisket, ribs, tallow fries, slaw, bread.'}, {id:'d5-pasta', name:'Pasta Bar', emoji:'🍝',start:1020, end:1170, tags:['V'], desc:'Fresh pasta, garden vegetables, olive oil.'} - ]}, + ]} ] }; FEST.days.forEach((d,i)=>{ const L=dayLabel(i); d.name=L.name; d.date=L.date; d.iso=L.iso; }); @@ -394,7 +404,8 @@ function renderTimeline(){ '
'+ '
'+item.emoji+'
'+ '
'+item.name+' '+item.tags.map(t=>''+t+'').join('')+'
'+ - '
'+item.desc+'
'+ + '
'+item.desc+'
'+ + '
📍 '+(STAND_LOCATIONS[item.name]||'location TBD')+'
'+ ''+ '
'+ ''+st.label+''+ diff --git a/server/server.js b/server/server.js index 4f4259e..c52543a 100644 --- a/server/server.js +++ b/server/server.js @@ -30,7 +30,6 @@ const DEFAULT_LOCATION = 'Center of camp, by the big solar dish'; // the app says where to pick up (success screen, orders view, staff board). const STAND_LOCATIONS = { 'Coffee Station': 'PLACEHOLDER — east path, next to the water station', - 'Sweets Cart': 'PLACEHOLDER — north gate, under the yellow canopy', 'Taco Stand': 'PLACEHOLDER — main lawn, food row #1', 'BBQ Pit': 'PLACEHOLDER — downwind corner behind the wood pile', 'Chicken Grill': 'PLACEHOLDER — main lawn, food row #2',