[object Object]

← back to Dw Marketing Reels

DW Marketing new-arrivals reels: HyperFrames reel generator + auth-gated console (:9848)

fc8c20e5706c54d57ac06e4be860e84b4740760c · 2026-07-12 10:20:18 -0700 · Steve Abrams

Files touched

Diff

commit fc8c20e5706c54d57ac06e4be860e84b4740760c
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jul 12 10:20:18 2026 -0700

    DW Marketing new-arrivals reels: HyperFrames reel generator + auth-gated console (:9848)
---
 .deploy.conf                   |   3 +
 .gitignore                     |  10 ++
 data/new-arrivals.json         | 367 +++++++++++++++++++++++++++++++++++++++++
 data/reels.json                |  16 ++
 package.json                   |  12 ++
 public/index.html              | 144 ++++++++++++++++
 scripts/build-reel.mjs         | 195 ++++++++++++++++++++++
 scripts/fetch-new-arrivals.mjs |  69 ++++++++
 server.js                      |  92 +++++++++++
 9 files changed, 908 insertions(+)

diff --git a/.deploy.conf b/.deploy.conf
new file mode 100644
index 0000000..3da0f91
--- /dev/null
+++ b/.deploy.conf
@@ -0,0 +1,3 @@
+PROJECT_NAME=dw-marketing-reels
+DEPLOY_PATH=/root/public-projects/dw-marketing-reels
+HEALTH_URL=http://127.0.0.1:9848/
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8af7985
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+reels/*.mp4
+hf-workspace/
diff --git a/data/new-arrivals.json b/data/new-arrivals.json
new file mode 100644
index 0000000..fec604b
--- /dev/null
+++ b/data/new-arrivals.json
@@ -0,0 +1,367 @@
+{
+  "source": "https://designerwallcoverings.com/collections/new-arrivals",
+  "fetched_at": "2026-07-12T17:14:36.543Z",
+  "count": 24,
+  "items": [
+    {
+      "id": 7882783621171,
+      "handle": "katamari-pewter-dwtt-74068",
+      "title": "Katamari, Pewter Wallcoverings | Thibaut",
+      "pattern": "Katamari",
+      "color": "Pewter",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34074_97ea7996-88e6-40bb-8df7-39d8ec10245c.jpg?v=1783874418",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34074_97ea7996-88e6-40bb-8df7-39d8ec10245c_1200x.jpg?v=1783874418",
+      "price": "130.32",
+      "url": "https://designerwallcoverings.com/products/katamari-pewter-dwtt-74068",
+      "created_at": "2026-07-12T09:40:15-07:00",
+      "published_at": "2026-07-12T09:40:17-07:00"
+    },
+    {
+      "id": 7882783588403,
+      "handle": "tenaya-beige-dwtt-74067",
+      "title": "Tenaya, Beige Wallcoverings | Thibaut",
+      "pattern": "Tenaya",
+      "color": "Beige",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34072.jpg?v=1783874414",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34072_1200x.jpg?v=1783874414",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/tenaya-beige-dwtt-74067",
+      "created_at": "2026-07-12T09:40:12-07:00",
+      "published_at": "2026-07-12T09:40:13-07:00"
+    },
+    {
+      "id": 7882783555635,
+      "handle": "katamari-dark-gold-dwtt-74066",
+      "title": "Katamari, Dark Gold Wallcoverings | Thibaut",
+      "pattern": "Katamari",
+      "color": "Dark Gold",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34073_f3da0951-bdd3-4e1f-ae08-1b47721a0f6e.jpg?v=1783874411",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34073_f3da0951-bdd3-4e1f-ae08-1b47721a0f6e_1200x.jpg?v=1783874411",
+      "price": "130.32",
+      "url": "https://designerwallcoverings.com/products/katamari-dark-gold-dwtt-74066",
+      "created_at": "2026-07-12T09:40:07-07:00",
+      "published_at": "2026-07-12T09:40:09-07:00"
+    },
+    {
+      "id": 7882775527475,
+      "handle": "tenaya-cream-dwtt-74065",
+      "title": "Tenaya, Cream Wallcoverings | Thibaut",
+      "pattern": "Tenaya",
+      "color": "Cream",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34071.jpg?v=1783870817",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34071_1200x.jpg?v=1783870817",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/tenaya-cream-dwtt-74065",
+      "created_at": "2026-07-12T08:40:15-07:00",
+      "published_at": "2026-07-12T08:40:16-07:00"
+    },
+    {
+      "id": 7882775494707,
+      "handle": "tenaya-spa-blue-dwtt-74064",
+      "title": "Tenaya, Spa Blue Wallcoverings | Thibaut",
+      "pattern": "Tenaya",
+      "color": "Spa Blue",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34070_81f41471-3b8c-4d53-9e8f-2f42ecdeec4c.jpg?v=1783870815",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34070_81f41471-3b8c-4d53-9e8f-2f42ecdeec4c_1200x.jpg?v=1783870815",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/tenaya-spa-blue-dwtt-74064",
+      "created_at": "2026-07-12T08:40:11-07:00",
+      "published_at": "2026-07-12T08:40:12-07:00"
+    },
+    {
+      "id": 7882775429171,
+      "handle": "tenaya-sage-dwtt-74063",
+      "title": "Tenaya, Sage Wallcoverings | Thibaut",
+      "pattern": "Tenaya",
+      "color": "Sage",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34069_cbc48cb4-4dee-41ff-8429-92859c80b6fc.jpg?v=1783870810",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34069_cbc48cb4-4dee-41ff-8429-92859c80b6fc_1200x.jpg?v=1783870810",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/tenaya-sage-dwtt-74063",
+      "created_at": "2026-07-12T08:40:07-07:00",
+      "published_at": "2026-07-12T08:40:09-07:00"
+    },
+    {
+      "id": 7882766319667,
+      "handle": "tenaya-fog-dwtt-74062",
+      "title": "Tenaya, Fog Wallcoverings | Thibaut",
+      "pattern": "Tenaya",
+      "color": "Fog",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34068_07acc6ed-ec25-47bf-b2d8-b1b4e4987425.jpg?v=1783867217",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34068_07acc6ed-ec25-47bf-b2d8-b1b4e4987425_1200x.jpg?v=1783867217",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/tenaya-fog-dwtt-74062",
+      "created_at": "2026-07-12T07:40:15-07:00",
+      "published_at": "2026-07-12T07:40:16-07:00"
+    },
+    {
+      "id": 7882766286899,
+      "handle": "tenaya-dusk-dwtt-74061",
+      "title": "Tenaya, Dusk Wallcoverings | Thibaut",
+      "pattern": "Tenaya",
+      "color": "Dusk",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34066.jpg?v=1783867214",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34066_1200x.jpg?v=1783867214",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/tenaya-dusk-dwtt-74061",
+      "created_at": "2026-07-12T07:40:11-07:00",
+      "published_at": "2026-07-12T07:40:13-07:00"
+    },
+    {
+      "id": 7882766221363,
+      "handle": "tenaya-mineral-dwtt-74060",
+      "title": "Tenaya, Mineral Wallcoverings | Thibaut",
+      "pattern": "Tenaya",
+      "color": "Mineral",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34067_5ce89ab8-b1e2-41d1-aeeb-b565f9869c3a.jpg?v=1783867210",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34067_5ce89ab8-b1e2-41d1-aeeb-b565f9869c3a_1200x.jpg?v=1783867210",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/tenaya-mineral-dwtt-74060",
+      "created_at": "2026-07-12T07:40:07-07:00",
+      "published_at": "2026-07-12T07:40:09-07:00"
+    },
+    {
+      "id": 7882736992307,
+      "handle": "linen-weave-white-dwtt-74059",
+      "title": "Linen Weave, White Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "White",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34057.jpg?v=1783863619",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34057_1200x.jpg?v=1783863619",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-white-dwtt-74059",
+      "created_at": "2026-07-12T06:40:16-07:00",
+      "published_at": "2026-07-12T06:40:18-07:00"
+    },
+    {
+      "id": 7882736926771,
+      "handle": "linen-weave-sand-dwtt-74058",
+      "title": "Linen Weave, Sand Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Sand",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34056.jpg?v=1783863615",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34056_1200x.jpg?v=1783863615",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-sand-dwtt-74058",
+      "created_at": "2026-07-12T06:40:12-07:00",
+      "published_at": "2026-07-12T06:40:14-07:00"
+    },
+    {
+      "id": 7882736894003,
+      "handle": "linen-weave-off-white-dwtt-74057",
+      "title": "Linen Weave, Off White Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Off White",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34055_36e39e93-f3c6-4302-8221-3fff084c51f7.jpg?v=1783863610",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34055_36e39e93-f3c6-4302-8221-3fff084c51f7_1200x.jpg?v=1783863610",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-off-white-dwtt-74057",
+      "created_at": "2026-07-12T06:40:07-07:00",
+      "published_at": "2026-07-12T06:40:10-07:00"
+    },
+    {
+      "id": 7882719592499,
+      "handle": "linen-weave-tan-dwtt-74056",
+      "title": "Linen Weave, Tan Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Tan",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34053.jpg?v=1783860018",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34053_1200x.jpg?v=1783860018",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-tan-dwtt-74056",
+      "created_at": "2026-07-12T05:40:16-07:00",
+      "published_at": "2026-07-12T05:40:17-07:00"
+    },
+    {
+      "id": 7882719330355,
+      "handle": "linen-weave-caramel-dwtt-74055",
+      "title": "Linen Weave, Caramel Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Caramel",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34054_28c6d832-5378-4895-84c6-cbbf10abff30.jpg?v=1783860016",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34054_28c6d832-5378-4895-84c6-cbbf10abff30_1200x.jpg?v=1783860016",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-caramel-dwtt-74055",
+      "created_at": "2026-07-12T05:40:12-07:00",
+      "published_at": "2026-07-12T05:40:13-07:00"
+    },
+    {
+      "id": 7882719035443,
+      "handle": "linen-weave-dark-grey-dwtt-74054",
+      "title": "Linen Weave, Dark Grey Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Dark Grey",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34052.jpg?v=1783860010",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34052_1200x.jpg?v=1783860010",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-dark-grey-dwtt-74054",
+      "created_at": "2026-07-12T05:40:07-07:00",
+      "published_at": "2026-07-12T05:40:10-07:00"
+    },
+    {
+      "id": 7882595565619,
+      "handle": "linen-weave-black-dwtt-74053",
+      "title": "Linen Weave, Black Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Black",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34051_e6178bb0-59ba-4df3-8dfa-3c50ca282d50.jpg?v=1783856413",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34051_e6178bb0-59ba-4df3-8dfa-3c50ca282d50_1200x.jpg?v=1783856413",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-black-dwtt-74053",
+      "created_at": "2026-07-12T04:40:10-07:00",
+      "published_at": "2026-07-12T04:40:11-07:00"
+    },
+    {
+      "id": 7882595467315,
+      "handle": "linen-weave-navy-dwtt-74052",
+      "title": "Linen Weave, Navy Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Navy",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34050_dcbe3a42-e382-4371-abce-dd54f0c53594.jpg?v=1783856409",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34050_dcbe3a42-e382-4371-abce-dd54f0c53594_1200x.jpg?v=1783856409",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-navy-dwtt-74052",
+      "created_at": "2026-07-12T04:40:06-07:00",
+      "published_at": "2026-07-12T04:40:08-07:00"
+    },
+    {
+      "id": 7882595303475,
+      "handle": "linen-weave-mineral-dwtt-74051",
+      "title": "Linen Weave, Mineral Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Mineral",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34048_5758df5d-e9ce-4447-8604-7fd85aab83e4.jpg?v=1783856405",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34048_5758df5d-e9ce-4447-8604-7fd85aab83e4_1200x.jpg?v=1783856405",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-mineral-dwtt-74051",
+      "created_at": "2026-07-12T04:40:02-07:00",
+      "published_at": "2026-07-12T04:40:04-07:00"
+    },
+    {
+      "id": 7882477436979,
+      "handle": "linen-weave-blue-dwtt-74050",
+      "title": "Linen Weave, Blue Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Blue",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34049_4b06c03a-9387-429d-9cef-472a006dfd4f.jpg?v=1783852818",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34049_4b06c03a-9387-429d-9cef-472a006dfd4f_1200x.jpg?v=1783852818",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-blue-dwtt-74050",
+      "created_at": "2026-07-12T03:40:16-07:00",
+      "published_at": "2026-07-12T03:40:17-07:00"
+    },
+    {
+      "id": 7882476585011,
+      "handle": "linen-weave-robin-s-egg-dwtt-74049",
+      "title": "Linen Weave, Robin's Egg Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Robin's Egg",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34047_80482a6f-e1ee-49a9-8059-5fbc282a5471.jpg?v=1783852816",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34047_80482a6f-e1ee-49a9-8059-5fbc282a5471_1200x.jpg?v=1783852816",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-robin-s-egg-dwtt-74049",
+      "created_at": "2026-07-12T03:40:12-07:00",
+      "published_at": "2026-07-12T03:40:13-07:00"
+    },
+    {
+      "id": 7882476519475,
+      "handle": "linen-weave-pine-green-dwtt-74048",
+      "title": "Linen Weave, Pine Green Wallcoverings | Thibaut",
+      "pattern": "Linen Weave",
+      "color": "Pine Green",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34046_9db61692-640e-4db1-a993-2ed542712fa6.jpg?v=1783852810",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34046_9db61692-640e-4db1-a993-2ed542712fa6_1200x.jpg?v=1783852810",
+      "price": "141.72",
+      "url": "https://designerwallcoverings.com/products/linen-weave-pine-green-dwtt-74048",
+      "created_at": "2026-07-12T03:40:07-07:00",
+      "published_at": "2026-07-12T03:40:09-07:00"
+    },
+    {
+      "id": 7882353999923,
+      "handle": "bristlecone-green-and-black-dwtt-74047",
+      "title": "Bristlecone, Green and Black Wallcoverings | Thibaut",
+      "pattern": "Bristlecone",
+      "color": "Green and Black",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34034.jpg?v=1783849218",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34034_1200x.jpg?v=1783849218",
+      "price": "125.43",
+      "url": "https://designerwallcoverings.com/products/bristlecone-green-and-black-dwtt-74047",
+      "created_at": "2026-07-12T02:40:15-07:00",
+      "published_at": "2026-07-12T02:40:17-07:00"
+    },
+    {
+      "id": 7882353967155,
+      "handle": "bristlecone-navy-dwtt-74046",
+      "title": "Bristlecone, Navy Wallcoverings | Thibaut",
+      "pattern": "Bristlecone",
+      "color": "Navy",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34033_d2e7996b-e53c-470d-a98d-55408c090b02.jpg?v=1783849215",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34033_d2e7996b-e53c-470d-a98d-55408c090b02_1200x.jpg?v=1783849215",
+      "price": "125.43",
+      "url": "https://designerwallcoverings.com/products/bristlecone-navy-dwtt-74046",
+      "created_at": "2026-07-12T02:40:12-07:00",
+      "published_at": "2026-07-12T02:40:13-07:00"
+    },
+    {
+      "id": 7882353836083,
+      "handle": "bristlecone-grey-dwtt-74045",
+      "title": "Bristlecone, Grey Wallcoverings | Thibaut",
+      "pattern": "Bristlecone",
+      "color": "Grey",
+      "vendor": "Thibaut",
+      "product_type": "Wallcovering",
+      "image": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34032_832bf857-636c-4f0b-abc4-48c380b70592.jpg?v=1783849211",
+      "image_hi": "https://cdn.shopify.com/s/files/1/0015/4117/7456/files/T34032_832bf857-636c-4f0b-abc4-48c380b70592_1200x.jpg?v=1783849211",
+      "price": "125.43",
+      "url": "https://designerwallcoverings.com/products/bristlecone-grey-dwtt-74045",
+      "created_at": "2026-07-12T02:40:07-07:00",
+      "published_at": "2026-07-12T02:40:09-07:00"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/data/reels.json b/data/reels.json
new file mode 100644
index 0000000..be54f64
--- /dev/null
+++ b/data/reels.json
@@ -0,0 +1,16 @@
+[
+  {
+    "file": "new-arrivals-2026-07-12-17-17.mp4",
+    "created_at": "2026-07-12T17:17:27.415Z",
+    "products": 6,
+    "seconds": 19,
+    "titles": [
+      "Katamari / Pewter",
+      "Tenaya / Beige",
+      "Katamari / Dark Gold",
+      "Tenaya / Cream",
+      "Tenaya / Spa Blue",
+      "Tenaya / Sage"
+    ]
+  }
+]
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6b67ff0
--- /dev/null
+++ b/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "dw-marketing-reels",
+  "version": "0.1.0",
+  "private": true,
+  "type": "module",
+  "description": "DW Marketing — auto-generated New Arrivals reels (HyperFrames) + auth-gated console",
+  "scripts": {
+    "start": "node server.js",
+    "fetch": "node scripts/fetch-new-arrivals.mjs",
+    "build-reel": "node scripts/build-reel.mjs"
+  }
+}
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..c5733f7
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,144 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1" />
+<title>DW Marketing · New Arrivals Reels</title>
+<style>
+  :root { --ink:#1c1a17; --cream:#f4efe6; --gold:#b08d57; --line:#e2dccf; --cols:4; }
+  * { box-sizing:border-box; }
+  body { margin:0; font:15px/1.5 "Helvetica Neue",system-ui,sans-serif; color:var(--ink); background:var(--cream); }
+  header { display:flex; align-items:center; gap:20px; padding:20px 28px; border-bottom:1px solid var(--line); background:#fff; position:sticky; top:0; z-index:5; }
+  header .mark { font-family:"Didot","Bodoni 72",Georgia,serif; font-size:26px; }
+  header .mark b { color:var(--gold); font-weight:400; }
+  header .sub { font-size:12px; letter-spacing:.22em; text-transform:uppercase; color:#8a8272; }
+  header .spacer { flex:1; }
+  button { font:inherit; border:1px solid var(--ink); background:var(--ink); color:#fff; padding:9px 16px; border-radius:999px; cursor:pointer; letter-spacing:.04em; }
+  button.ghost { background:#fff; color:var(--ink); }
+  button:disabled { opacity:.5; cursor:not-allowed; }
+  main { padding:28px; max-width:1400px; margin:0 auto; }
+  h2 { font-family:"Didot","Bodoni 72",Georgia,serif; font-weight:400; font-size:26px; margin:34px 0 14px; }
+  .status { font-size:13px; color:#6a6355; min-height:20px; margin:6px 0 0; }
+  .reels { display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:20px; }
+  .reel { background:#fff; border:1px solid var(--line); border-radius:12px; overflow:hidden; }
+  .reel video { width:100%; display:block; background:#000; aspect-ratio:9/16; object-fit:cover; }
+  .reel .body { padding:12px 14px; }
+  .reel .when { font-size:12px; color:#8a8272; }
+  .reel .titles { font-size:12.5px; color:#4c463c; margin-top:6px; max-height:60px; overflow:hidden; }
+  .reel .row { display:flex; gap:10px; margin-top:10px; align-items:center; }
+  .reel a { font-size:12px; color:var(--gold); text-decoration:none; letter-spacing:.06em; text-transform:uppercase; }
+  .controls { display:flex; gap:22px; align-items:center; flex-wrap:wrap; margin:6px 0 16px; }
+  .controls label { font-size:12px; letter-spacing:.14em; text-transform:uppercase; color:#8a8272; margin-right:8px; }
+  select, input[type=range] { font:inherit; }
+  select { padding:7px 10px; border:1px solid var(--line); border-radius:8px; background:#fff; }
+  .grid { display:grid; grid-template-columns:repeat(var(--cols),1fr); gap:14px; }
+  .card { background:#fff; border:1px solid var(--line); border-radius:10px; overflow:hidden; }
+  .card .thumb { width:100%; aspect-ratio:4/5; object-fit:cover; display:block; background:#eee; }
+  .card .info { padding:9px 11px; }
+  .card .pat { font-family:"Didot","Bodoni 72",Georgia,serif; font-size:17px; }
+  .card .met { font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:var(--gold); margin-top:3px; }
+  .card .when { font-size:11px; color:#9a917f; margin-top:7px; }
+</style>
+</head>
+<body>
+<header>
+  <div>
+    <div class="mark">Designer <b>Wallcoverings</b></div>
+    <div class="sub">Marketing · New Arrivals Reels</div>
+  </div>
+  <div class="spacer"></div>
+  <button class="ghost" id="btnRefresh">↻ Refresh feed</button>
+  <button id="btnBuild">▶ Generate reel</button>
+</header>
+<main>
+  <div class="status" id="status"></div>
+
+  <h2>Reels</h2>
+  <div class="reels" id="reels"></div>
+
+  <h2>Current New Arrivals <span id="naCount" style="font-size:13px;color:#8a8272"></span></h2>
+  <div class="controls">
+    <div><label>Sort</label>
+      <select id="sort">
+        <option value="newest">Newest</option>
+        <option value="pattern">Pattern A→Z</option>
+        <option value="vendor">Vendor</option>
+        <option value="color">Color</option>
+      </select>
+    </div>
+    <div><label>Density</label>
+      <input type="range" id="density" min="2" max="7" step="1" />
+    </div>
+  </div>
+  <div class="grid" id="grid"></div>
+</main>
+
+<script>
+const $ = s => document.querySelector(s);
+const fmt = iso => { try { return new Date(iso).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); } catch { return iso||''; } };
+
+// ---- reels gallery ----
+async function loadReels() {
+  const reels = await (await fetch('api/reels')).json();
+  $('#reels').innerHTML = reels.length ? reels.map(r => `
+    <div class="reel">
+      <video src="reels/${encodeURIComponent(r.file)}" controls preload="metadata" playsinline></video>
+      <div class="body">
+        <div class="when" title="${r.created_at}">🕓 ${fmt(r.created_at)}</div>
+        <div class="titles">${(r.titles||[]).join(' · ')}</div>
+        <div class="row"><a href="reels/${encodeURIComponent(r.file)}" download>Download</a>
+          <span style="font-size:12px;color:#9a917f">${r.products} products · ${r.seconds}s</span></div>
+      </div>
+    </div>`).join('') : '<p style="color:#8a8272">No reels yet — hit “Generate reel”.</p>';
+}
+
+// ---- new arrivals grid (sort + density, persisted) ----
+let NA = [];
+function renderGrid() {
+  const s = $('#sort').value;
+  const items = [...NA];
+  const by = { pattern:(a,b)=>a.pattern.localeCompare(b.pattern), vendor:(a,b)=>(a.vendor||'').localeCompare(b.vendor||''),
+    color:(a,b)=>(a.color||'').localeCompare(b.color||''), newest:(a,b)=>new Date(b.created_at)-new Date(a.created_at) };
+  items.sort(by[s]||by.newest);
+  $('#grid').innerHTML = items.map(it => `
+    <div class="card">
+      <img class="thumb" loading="lazy" src="${it.image_hi||it.image}" alt="${it.pattern}" onerror="this.src='${it.image}'"/>
+      <div class="info">
+        <div class="pat">${it.pattern}</div>
+        <div class="met">${[it.color,it.vendor].filter(Boolean).join(' · ')}</div>
+        <div class="when" title="${it.created_at}">🕓 ${fmt(it.created_at)}</div>
+      </div>
+    </div>`).join('');
+}
+async function loadNA() {
+  const d = await (await fetch('api/new-arrivals')).json();
+  NA = d.items||[]; $('#naCount').textContent = `(${NA.length} · ${d.fetched_at?('feed '+fmt(d.fetched_at)):''})`;
+  renderGrid();
+}
+
+// persisted controls
+$('#sort').value = localStorage.getItem('dwr_sort')||'newest';
+$('#density').value = localStorage.getItem('dwr_density')||'4';
+document.documentElement.style.setProperty('--cols', $('#density').value);
+$('#sort').addEventListener('change', e=>{ localStorage.setItem('dwr_sort',e.target.value); renderGrid(); });
+$('#density').addEventListener('input', e=>{ localStorage.setItem('dwr_density',e.target.value); document.documentElement.style.setProperty('--cols', e.target.value); });
+
+// ---- jobs ----
+async function poll() {
+  const st = await (await fetch('api/status')).json();
+  const busy = st.building;
+  $('#btnBuild').disabled = busy; $('#btnRefresh').disabled = busy;
+  if (busy) { $('#status').textContent = '⏳ working… ' + (st.log.split('\n').filter(Boolean).slice(-1)[0]||''); setTimeout(poll, 1500); }
+  else if ($('#status').dataset.wasBusy==='1') { $('#status').dataset.wasBusy=''; $('#status').textContent='✓ done'; loadReels(); loadNA(); }
+}
+async function job(path, label) {
+  $('#status').textContent = label; $('#status').dataset.wasBusy='1';
+  await fetch(path,{method:'POST'}); poll();
+}
+$('#btnRefresh').addEventListener('click', ()=>job('api/refresh','↻ refreshing feed…'));
+$('#btnBuild').addEventListener('click', ()=>job('api/build','▶ generating reel (≈30s)…'));
+
+loadReels(); loadNA();
+</script>
+</body>
+</html>
diff --git a/scripts/build-reel.mjs b/scripts/build-reel.mjs
new file mode 100644
index 0000000..cee6084
--- /dev/null
+++ b/scripts/build-reel.mjs
@@ -0,0 +1,195 @@
+#!/usr/bin/env node
+// Generate a portrait "New Arrivals" reel composition from data/new-arrivals.json,
+// download product images locally (deterministic render), then render to MP4 via HyperFrames.
+// $0 — local ffmpeg/Chrome render, no paid API.
+import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, readdirSync, statSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+import { execSync } from 'node:child_process';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const WS = join(ROOT, 'hf-workspace');
+const N = Number(process.env.REEL_COUNT || 6);          // products per reel
+const PER = Number(process.env.REEL_SECS || 2.4);        // seconds per product
+const INTRO = 2.0, OUTRO = 2.6;
+
+const esc = s => String(s || '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
+
+async function dl(url, dest) {
+  try {
+    const r = await fetch(url, { headers: { 'User-Agent': 'dw-marketing-reels' } });
+    if (!r.ok) return false;
+    const buf = Buffer.from(await r.arrayBuffer());
+    if (buf.length < 1000) return false;
+    writeFileSync(dest, buf);
+    return true;
+  } catch { return false; }
+}
+
+async function main() {
+  const data = JSON.parse(readFileSync(join(ROOT, 'data', 'new-arrivals.json'), 'utf8'));
+  const items = data.items.slice(0, N);
+  if (!items.length) throw new Error('no items in data/new-arrivals.json — run fetch first');
+
+  const imgDir = join(WS, 'assets', 'img');
+  mkdirSync(imgDir, { recursive: true });
+  for (const [i, it] of items.entries()) {
+    const dest = join(imgDir, `p${i}.jpg`);
+    const ok = (await dl(it.image_hi, dest)) || (await dl(it.image, dest));
+    it._local = ok ? `assets/img/p${i}.jpg` : it.image;
+    console.log(`  img ${i}: ${ok ? '✓ local' : '↯ remote'} ${it.pattern}`);
+  }
+
+  const total = +(INTRO + items.length * PER + OUTRO).toFixed(2);
+  const dateStr = new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
+
+  // --- slides markup ---
+  const slides = items.map((it, i) => `
+    <div class="slide" id="slide${i}">
+      <img class="ph" src="${esc(it._local)}" alt="${esc(it.pattern)}" />
+      <div class="scrim"></div>
+      <div class="cap">
+        <div class="pattern">${esc(it.pattern)}</div>
+        <div class="meta">${esc([it.color, it.vendor].filter(Boolean).join(' · '))}</div>
+      </div>
+    </div>`).join('');
+
+  // --- gsap timeline (seek-safe) ---
+  const anim = items.map((it, i) => {
+    const at = +(INTRO + i * PER).toFixed(3);
+    return `
+    gsap.set("#slide${i}", { opacity: 0 });
+    gsap.set("#slide${i} .ph", { scale: 1.12 });
+    gsap.set("#slide${i} .cap", { y: 40, opacity: 0 });
+    tl.to("#slide${i}", { opacity: 1, duration: 0.5, ease: "power1.out" }, ${at});
+    tl.to("#slide${i} .ph", { scale: 1.0, duration: ${PER}, ease: "none" }, ${at});
+    tl.to("#slide${i} .cap", { y: 0, opacity: 1, duration: 0.6, ease: "power2.out" }, ${(at + 0.25).toFixed(3)});
+    tl.to("#slide${i}", { opacity: 0, duration: 0.45, ease: "power1.in" }, ${(at + PER - 0.35).toFixed(3)});`;
+  }).join('\n');
+
+  const outroAt = +(INTRO + items.length * PER).toFixed(3);
+
+  const html = `<!doctype html>
+<html lang="en" data-resolution="portrait">
+<head>
+<meta charset="UTF-8" />
+<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
+<style>
+  :root { --ink:#1c1a17; --cream:#f4efe6; --gold:#b08d57; }
+  html,body { margin:0; padding:0; width:1080px; height:1920px; overflow:hidden;
+    background:var(--ink); font-family:"Helvetica Neue",system-ui,sans-serif; }
+  #main-composition { position:relative; width:1080px; height:1920px; overflow:hidden; background:var(--ink); }
+  .slide { position:absolute; inset:0; }
+  .slide .ph { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; transform-origin:center; }
+  .scrim { position:absolute; inset:0; background:linear-gradient(180deg,rgba(0,0,0,.15) 0%,rgba(0,0,0,0) 38%,rgba(0,0,0,.72) 100%); }
+  .cap { position:absolute; left:72px; right:72px; bottom:150px; }
+  .cap .pattern { font-family:"Didot","Bodoni 72",Georgia,serif; font-size:96px; line-height:1.02;
+    color:#fff; letter-spacing:.01em; text-shadow:0 2px 20px rgba(0,0,0,.5); }
+  .cap .meta { margin-top:18px; font-size:34px; letter-spacing:.22em; text-transform:uppercase; color:var(--gold); }
+  /* intro / outro cards */
+  .card { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
+    text-align:center; background:var(--cream); color:var(--ink); }
+  .card .kicker { font-size:32px; letter-spacing:.42em; text-transform:uppercase; color:var(--gold); }
+  .card .big { font-family:"Didot","Bodoni 72",Georgia,serif; font-size:150px; line-height:.98; margin:26px 60px; }
+  .card .sub { font-size:34px; letter-spacing:.16em; text-transform:uppercase; color:#5a544c; }
+  .card .rule { width:120px; height:2px; background:var(--gold); margin:44px 0; }
+  .card .url { position:absolute; bottom:150px; font-size:30px; letter-spacing:.24em; text-transform:uppercase; color:var(--ink); }
+</style>
+</head>
+<body>
+  <div id="main-composition" data-composition-id="main-video"
+       data-width="1080" data-height="1920" data-start="0" data-duration="${total}">
+
+    <div class="card" id="intro">
+      <div class="kicker">New Arrivals</div>
+      <div class="big">Just In</div>
+      <div class="rule"></div>
+      <div class="sub">Designer Wallcoverings · ${esc(dateStr)}</div>
+    </div>
+
+    ${slides}
+
+    <div class="card" id="outro">
+      <div class="kicker">Shop the collection</div>
+      <div class="big">Designer<br/>Wallcoverings</div>
+      <div class="url">designerwallcoverings.com</div>
+    </div>
+
+    <script>
+      const tl = gsap.timeline({ paused: true });
+      window.__timelines = window.__timelines || {};
+
+      // intro
+      gsap.set("#intro .big", { y: 40, opacity: 0 });
+      gsap.set("#intro .kicker, #intro .sub, #intro .rule", { opacity: 0 });
+      tl.to("#intro .kicker", { opacity: 1, duration: 0.5 }, 0.2);
+      tl.to("#intro .big", { opacity: 1, y: 0, duration: 0.7, ease: "power2.out" }, 0.4);
+      tl.to("#intro .rule, #intro .sub", { opacity: 1, duration: 0.5 }, 0.9);
+      tl.to("#intro", { opacity: 0, duration: 0.5, ease: "power1.in" }, ${(INTRO - 0.4).toFixed(2)});
+
+      // slides
+${anim}
+
+      // outro
+      gsap.set("#outro", { opacity: 0 });
+      gsap.set("#outro .big", { y: 40, opacity: 0 });
+      gsap.set("#outro .url", { opacity: 0 });
+      tl.to("#outro", { opacity: 1, duration: 0.5 }, ${outroAt.toFixed(2)});
+      tl.to("#outro .big", { opacity: 1, y: 0, duration: 0.7, ease: "power2.out" }, ${(outroAt + 0.2).toFixed(2)});
+      tl.to("#outro .url", { opacity: 1, duration: 0.5 }, ${(outroAt + 0.7).toFixed(2)});
+
+      window.__timelines["main-video"] = tl;
+    </script>
+  </div>
+</body>
+</html>`;
+
+  writeFileSync(join(WS, 'index.html'), html);
+  // strip the starter sub-compositions so they don't get pulled in
+  for (const f of ['compositions/intro.html', 'compositions/graphics.html', 'compositions/captions.html']) {
+    const p = join(WS, f); if (existsSync(p)) writeFileSync(p, '<div></div>');
+  }
+  console.log(`✓ composition written (${items.length} products, ${total}s portrait)`);
+
+  // --- render ---
+  console.log('→ rendering (local ffmpeg/Chrome, $0)…');
+  execSync('npx --yes hyperframes render', { cwd: WS, stdio: 'inherit', env: { ...process.env, HYPERFRAMES_SKIP_SKILLS: '1' } });
+
+  // move newest mp4 to reels/
+  const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, '-');
+  const outName = `new-arrivals-${stamp}.mp4`;
+  const found = findMp4(WS);
+  if (found) {
+    mkdirSync(join(ROOT, 'reels'), { recursive: true });
+    copyFileSync(found, join(ROOT, 'reels', outName));
+    // manifest for the gallery
+    const manPath = join(ROOT, 'data', 'reels.json');
+    const man = existsSync(manPath) ? JSON.parse(readFileSync(manPath, 'utf8')) : [];
+    man.unshift({ file: outName, created_at: new Date().toISOString(), products: items.length,
+      seconds: total, titles: items.map(i => `${i.pattern} / ${i.color}`) });
+    writeFileSync(manPath, JSON.stringify(man, null, 2));
+    console.log(`✓ reel -> reels/${outName}`);
+  } else {
+    console.error('✗ no MP4 produced');
+    process.exit(2);
+  }
+}
+
+function findMp4(dir) {
+  let newest = null, mt = 0;
+  const walk = d => {
+    for (const e of readdirSync(d, { withFileTypes: true })) {
+      if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
+      const p = join(d, e.name);
+      if (e.isDirectory()) walk(p);
+      else if (e.name.endsWith('.mp4')) {
+        const t = statSync(p).mtimeMs;
+        if (t > mt) { mt = t; newest = p; }
+      }
+    }
+  };
+  walk(dir);
+  return newest;
+}
+
+main().catch(e => { console.error('✗', e.message); process.exit(1); });
diff --git a/scripts/fetch-new-arrivals.mjs b/scripts/fetch-new-arrivals.mjs
new file mode 100644
index 0000000..d8c100e
--- /dev/null
+++ b/scripts/fetch-new-arrivals.mjs
@@ -0,0 +1,69 @@
+#!/usr/bin/env node
+// Pull the LIVE DW "New Arrivals" collection (maintained by the new-arrivals-rotator
+// skill) and normalize it into data/new-arrivals.json for the reel builder + gallery.
+// $0 — public storefront products.json, no auth, no paid API.
+import { writeFileSync, mkdirSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const STORE = process.env.DW_STORE || 'https://designerwallcoverings.com';
+const COLLECTION = process.env.DW_COLLECTION || 'new-arrivals';
+const LIMIT = Number(process.env.DW_LIMIT || 24);
+const UA = 'Mozilla/5.0 (dw-marketing-reels)';
+
+function cleanTitle(t = '') {
+  // "Katamari, Pewter Wallcoverings | Thibaut" -> {pattern:"Katamari", color:"Pewter", vendor:"Thibaut"}
+  const [main, vendor = ''] = t.split('|').map(s => s.trim());
+  const noSuffix = main.replace(/\s*(Wallcoverings?|Wallpaper|Fabric)s?\s*$/i, '').trim();
+  const [pattern, ...rest] = noSuffix.split(',');
+  return {
+    pattern: (pattern || noSuffix).trim(),
+    color: rest.join(',').trim(),
+    vendor: vendor.trim(),
+  };
+}
+
+async function main() {
+  const url = `${STORE}/collections/${COLLECTION}/products.json?limit=${LIMIT}`;
+  const res = await fetch(url, { headers: { 'User-Agent': UA } });
+  if (!res.ok) throw new Error(`fetch ${url} -> ${res.status}`);
+  const { products = [] } = await res.json();
+
+  const items = products
+    .map(p => {
+      const img = (p.images || [])[0]?.src || '';
+      const variant = (p.variants || [])[0] || {};
+      const t = cleanTitle(p.title);
+      return {
+        id: p.id,
+        handle: p.handle,
+        title: p.title,
+        pattern: t.pattern,
+        color: t.color,
+        vendor: p.vendor || t.vendor,
+        product_type: p.product_type || '',
+        image: img,
+        // large render size for crisp video frames
+        image_hi: img ? img.replace(/(\.[a-z]+)(\?.*)?$/i, '_1200x$1$2') : '',
+        price: variant.price || null,
+        url: `${STORE}/products/${p.handle}`,
+        created_at: p.created_at,
+        published_at: p.published_at,
+      };
+    })
+    .filter(x => x.image); // a reel frame needs an image
+
+  mkdirSync(join(ROOT, 'data'), { recursive: true });
+  const out = {
+    source: `${STORE}/collections/${COLLECTION}`,
+    fetched_at: new Date().toISOString(),
+    count: items.length,
+    items,
+  };
+  writeFileSync(join(ROOT, 'data', 'new-arrivals.json'), JSON.stringify(out, null, 2));
+  console.log(`✓ ${items.length} new arrivals -> data/new-arrivals.json`);
+  items.slice(0, 5).forEach(i => console.log(`  • ${i.pattern} / ${i.color} (${i.vendor})`));
+}
+
+main().catch(e => { console.error('✗', e.message); process.exit(1); });
diff --git a/server.js b/server.js
new file mode 100644
index 0000000..c58c701
--- /dev/null
+++ b/server.js
@@ -0,0 +1,92 @@
+#!/usr/bin/env node
+// DW Marketing — New Arrivals Reels console.
+// Zero-dependency Node http server behind HTTP Basic Auth. Serves the reel gallery,
+// the current New Arrivals grid (sort + density per standing rule), and lets marketing
+// refresh the feed + generate a new reel on demand ($0 local render).
+import http from 'node:http';
+import { readFile, stat } from 'node:fs/promises';
+import { createReadStream, existsSync } from 'node:fs';
+import { spawn } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { dirname, join, extname, normalize } from 'node:path';
+
+const ROOT = dirname(fileURLToPath(import.meta.url));
+const PORT = Number(process.env.PORT || 9846);
+const USER = process.env.MKT_USER || 'admin';
+const PASS = process.env.MKT_PASS || 'DW2024!';
+const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css',
+  '.json': 'application/json', '.mp4': 'video/mp4', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml' };
+
+let building = false;
+let lastLog = '';
+
+function authed(req) {
+  const h = req.headers.authorization || '';
+  if (!h.startsWith('Basic ')) return false;
+  const [u, p] = Buffer.from(h.slice(6), 'base64').toString().split(':');
+  return u === USER && p === PASS;
+}
+
+function run(script, res) {
+  if (building) { res.writeHead(409).end(JSON.stringify({ error: 'a job is already running' })); return; }
+  building = true; lastLog = '';
+  const env = { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env.PATH || ''}`, HYPERFRAMES_SKIP_SKILLS: '1' };
+  const ch = spawn(process.execPath, [join(ROOT, 'scripts', script)], { cwd: ROOT, env });
+  ch.stdout.on('data', d => { lastLog += d; });
+  ch.stderr.on('data', d => { lastLog += d; });
+  ch.on('close', code => { building = false; lastLog += `\n[exit ${code}]`; });
+  res.writeHead(202, { 'content-type': 'application/json' }).end(JSON.stringify({ started: script }));
+}
+
+async function serveFile(fp, req, res) {
+  const st = await stat(fp);
+  const type = MIME[extname(fp)] || 'application/octet-stream';
+  // range support so <video> can scrub in every browser
+  const range = req.headers.range;
+  if (range && type === 'video/mp4') {
+    const m = /bytes=(\d+)-(\d*)/.exec(range);
+    const start = Number(m[1]); const end = m[2] ? Number(m[2]) : st.size - 1;
+    res.writeHead(206, { 'content-type': type, 'accept-ranges': 'bytes',
+      'content-range': `bytes ${start}-${end}/${st.size}`, 'content-length': end - start + 1 });
+    createReadStream(fp, { start, end }).pipe(res); return;
+  }
+  res.writeHead(200, { 'content-type': type, 'content-length': st.size, 'accept-ranges': 'bytes' });
+  createReadStream(fp).pipe(res);
+}
+
+const server = http.createServer(async (req, res) => {
+  if (!authed(req)) {
+    res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="DW Marketing"' }).end('Auth required');
+    return;
+  }
+  const url = new URL(req.url, `http://localhost`);
+  const p = url.pathname;
+  try {
+    if (p === '/' ) return await serveFile(join(ROOT, 'public', 'index.html'), req, res);
+    if (p === '/api/reels') {
+      const fp = join(ROOT, 'data', 'reels.json');
+      return res.writeHead(200, { 'content-type': 'application/json' }).end(existsSync(fp) ? await readFile(fp) : '[]');
+    }
+    if (p === '/api/new-arrivals') {
+      const fp = join(ROOT, 'data', 'new-arrivals.json');
+      return res.writeHead(200, { 'content-type': 'application/json' }).end(existsSync(fp) ? await readFile(fp) : '{"items":[]}');
+    }
+    if (p === '/api/status') return res.writeHead(200, { 'content-type': 'application/json' })
+      .end(JSON.stringify({ building, log: lastLog.slice(-4000) }));
+    if (p === '/api/refresh' && req.method === 'POST') return run('fetch-new-arrivals.mjs', res);
+    if (p === '/api/build' && req.method === 'POST') return run('build-reel.mjs', res);
+    if (p.startsWith('/reels/')) {
+      const fp = normalize(join(ROOT, 'reels', p.slice('/reels/'.length)));
+      if (fp.startsWith(join(ROOT, 'reels')) && existsSync(fp)) return await serveFile(fp, req, res);
+    }
+    if (p.startsWith('/public/')) {
+      const fp = normalize(join(ROOT, p));
+      if (fp.startsWith(join(ROOT, 'public')) && existsSync(fp)) return await serveFile(fp, req, res);
+    }
+    res.writeHead(404).end('not found');
+  } catch (e) {
+    res.writeHead(500).end('error: ' + e.message);
+  }
+});
+
+server.listen(PORT, () => console.log(`DW Marketing reels console → http://127.0.0.1:${PORT}  (auth ${USER})`));

(oldest)  ·  back to Dw Marketing Reels  ·  Add nightly runner (gated plist), README, deploy config 7630cbd →