[object Object]

← back to Wallco Ai

furnish: disk-backed cache (data/furnish-cache/<id>-<ctx>.json)

f4031b41d8b2fdc5f0b4e6f59b667d2def629c80 · 2026-05-13 14:10:05 -0700 · SteveStudio2

Per Steve's cost note — each click was hitting Gemini-text fresh. Now:
- First click for (design, context) → Gemini call + cache write
- Every subsequent click → instant cache hit, zero cost
- ?refresh=1 forces re-gen (admin escape hatch)
- TTL: forever — designs don't change; LLM output for same inputs is
  stable enough to never expire
- Response includes cached:true so UI can show 'instant' if desired

Cost impact: at 1000 furnish clicks/mo with say 100 unique (design,ctx)
pairs, drops from $1/mo to $0.10/mo on text alone.

Files touched

Diff

commit f4031b41d8b2fdc5f0b4e6f59b667d2def629c80
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 13 14:10:05 2026 -0700

    furnish: disk-backed cache (data/furnish-cache/<id>-<ctx>.json)
    
    Per Steve's cost note — each click was hitting Gemini-text fresh. Now:
    - First click for (design, context) → Gemini call + cache write
    - Every subsequent click → instant cache hit, zero cost
    - ?refresh=1 forces re-gen (admin escape hatch)
    - TTL: forever — designs don't change; LLM output for same inputs is
      stable enough to never expire
    - Response includes cached:true so UI can show 'instant' if desired
    
    Cost impact: at 1000 furnish clicks/mo with say 100 unique (design,ctx)
    pairs, drops from $1/mo to $0.10/mo on text alone.
---
 .gitignore        |   1 +
 data/designs.json | 220 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 server.js         |  29 ++++++-
 3 files changed, 249 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 5ec5329..d8a671a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,4 @@ data/generated_pre_seamless_backup/
 data/moodboards.json
 data/reviews.json
 data/sample-requests.jsonl
+data/furnish-cache/
diff --git a/data/designs.json b/data/designs.json
index 42bfb19..b61e702 100644
--- a/data/designs.json
+++ b/data/designs.json
@@ -82801,5 +82801,225 @@
     "user_scale_good": null,
     "user_stars": null,
     "room_mockups": []
+  },
+  {
+    "id": 3993,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#8a7053",
+    "saturation": 0.249,
+    "title": "Amber Atelier No.3993",
+    "handle": "wallco-3993",
+    "image_url": "/designs/img/1778706472331_1277553684.png",
+    "filename": "1778706472331_1277553684.png",
+    "generator": "wallco.ai",
+    "seed": 1277553684,
+    "is_published": false,
+    "created_at": "2026-05-13T21:08:09.673476+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
+  },
+  {
+    "id": 3994,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#8a7053",
+    "saturation": 0.249,
+    "title": "Amber Salon No.3994",
+    "handle": "wallco-3994",
+    "image_url": "/designs/img/var_3993_hue+60_1778706489917.png",
+    "filename": "var_3993_hue+60_1778706489917.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-13T21:08:15.85105+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
+  },
+  {
+    "id": 3995,
+    "kind": "seamless_tile",
+    "category": "stoned-animals",
+    "dominant_hex": "#527345",
+    "saturation": 0.25,
+    "title": "Sage Hazefield No.3995",
+    "handle": "wallco-3995",
+    "image_url": "/designs/img/1778706485153_264367489.png",
+    "filename": "1778706485153_264367489.png",
+    "generator": "wallco.ai",
+    "seed": 264367489,
+    "is_published": false,
+    "created_at": "2026-05-13T21:08:15.853156+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
+  },
+  {
+    "id": 3996,
+    "kind": "seamless_tile",
+    "category": "drunk-animals",
+    "dominant_hex": "#5f3b62",
+    "saturation": 0.248,
+    "title": "Plum Carouse No.3996",
+    "handle": "wallco-3996",
+    "image_url": "/designs/img/1778706485153_294943035.png",
+    "filename": "1778706485153_294943035.png",
+    "generator": "wallco.ai",
+    "seed": 294943035,
+    "is_published": false,
+    "created_at": "2026-05-13T21:08:17.12323+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
+  },
+  {
+    "id": 3997,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#8a7053",
+    "saturation": 0.249,
+    "title": "Amber Heirloom No.3997",
+    "handle": "wallco-3997",
+    "image_url": "/designs/img/var_3993_hue+120_1778706495874.png",
+    "filename": "var_3993_hue+120_1778706495874.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-13T21:08:18.538094+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
+  },
+  {
+    "id": 3998,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#8a7053",
+    "saturation": 0.249,
+    "title": "Amber Atelier No.3998",
+    "handle": "wallco-3998",
+    "image_url": "/designs/img/var_3993_hue+180_1778706498543.png",
+    "filename": "var_3993_hue+180_1778706498543.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-13T21:08:21.061595+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
+  },
+  {
+    "id": 3999,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#8a7053",
+    "saturation": 0.249,
+    "title": "Amber Salon No.3999",
+    "handle": "wallco-3999",
+    "image_url": "/designs/img/var_3993_hue-60_1778706501074.png",
+    "filename": "var_3993_hue-60_1778706501074.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-13T21:08:24.990778+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
+  },
+  {
+    "id": 4000,
+    "kind": "seamless_tile",
+    "category": "damask",
+    "dominant_hex": "#8a7053",
+    "saturation": 0.249,
+    "title": "Amber Baroque No.4000",
+    "handle": "wallco-4000",
+    "image_url": "/designs/img/var_3993_mono_1778706505006.png",
+    "filename": "var_3993_mono_1778706505006.png",
+    "generator": "wallco.ai",
+    "seed": 0,
+    "is_published": false,
+    "created_at": "2026-05-13T21:08:28.434933+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
+  },
+  {
+    "id": 4001,
+    "kind": "seamless_tile",
+    "category": "drunk-animals",
+    "dominant_hex": "#723e5d",
+    "saturation": 0.295,
+    "title": "Plum Tipple No.4001",
+    "handle": "wallco-4001",
+    "image_url": "/designs/img/1778706571763_1580835151.png",
+    "filename": "1778706571763_1580835151.png",
+    "generator": "wallco.ai",
+    "seed": 1580835151,
+    "is_published": false,
+    "created_at": "2026-05-13T21:09:40.313735+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
+  },
+  {
+    "id": 4002,
+    "kind": "seamless_tile",
+    "category": "stoned-animals",
+    "dominant_hex": "#596b64",
+    "saturation": 0.092,
+    "title": "Marine Stardust No.4002",
+    "handle": "wallco-4002",
+    "image_url": "/designs/img/1778706571763_1200344699.png",
+    "filename": "1778706571763_1200344699.png",
+    "generator": "wallco.ai",
+    "seed": 1200344699,
+    "is_published": false,
+    "created_at": "2026-05-13T21:09:43.483698+00:00",
+    "motifs": [],
+    "user_removed": false,
+    "user_color_good": null,
+    "user_style_good": null,
+    "user_scale_good": null,
+    "user_stars": null,
+    "room_mockups": []
   }
 ]
\ No newline at end of file
diff --git a/server.js b/server.js
index 3cd2579..6273943 100644
--- a/server.js
+++ b/server.js
@@ -12808,6 +12808,17 @@ app.get('/api/isolate/:designId', requireAdmin, (req, res) => {
 //    /commercial/hospitality/healthcare). Uses Gemini-text against the
 //    design's palette + motif + category. Read-only — no DB writes.
 //    furnish_route marker.
+// Disk-backed cache for /api/design/:id/furnish. The Gemini-text output
+// for a given (design × context) is stable — same palette + motif + use
+// case → same recommendations. Cache forever; manual purge via deleting
+// data/furnish-cache/<id>-<ctx>.json. Steve's 2026-05-13 cost note: each
+// click was hitting Gemini fresh; this drops repeat cost to zero.
+const FURNISH_CACHE_DIR = path.join(__dirname, 'data', 'furnish-cache');
+fs.mkdirSync(FURNISH_CACHE_DIR, { recursive: true });
+function furnishCachePath(id, ctx) {
+  return path.join(FURNISH_CACHE_DIR, `${id}-${ctx}.json`);
+}
+
 app.post('/api/design/:id/furnish', async (req, res) => {
   try {
     const id = parseInt(req.params.id, 10);
@@ -12816,6 +12827,15 @@ app.post('/api/design/:id/furnish', async (req, res) => {
     const validCtx = new Set(['residential','commercial','hospitality','healthcare']);
     if (!validCtx.has(ctx)) return res.status(400).json({ ok: false, error: 'bad context' });
 
+    // Cache hit — skip Gemini, return saved JSON. ?refresh=1 forces re-gen.
+    const cachePath = furnishCachePath(id, ctx);
+    if (req.query.refresh !== '1' && fs.existsSync(cachePath)) {
+      try {
+        const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
+        return res.json({ ...cached, cached: true });
+      } catch (e) { /* fall through to fresh gen on parse error */ }
+    }
+
     const d = DESIGNS.find(x => x.id === id);
     if (!d) return res.status(404).json({ ok: false, error: 'design not found' });
 
@@ -12858,7 +12878,14 @@ app.post('/api/design/:id/furnish', async (req, res) => {
     const text = j.candidates?.[0]?.content?.parts?.find(p => p.text)?.text || '{}';
     let parsed = {};
     try { parsed = JSON.parse(text); } catch (e) { return res.status(502).json({ ok: false, error: 'parse failed', text: text.slice(0,200) }); }
-    return res.json({ ok: true, design_id: id, context: ctx, categories: parsed.categories || [], note: parsed.note || '' });
+    const payload = { ok: true, design_id: id, context: ctx, categories: parsed.categories || [], note: parsed.note || '' };
+    // Write-through cache (best-effort; ignore disk errors)
+    try {
+      const tmp = cachePath + '.tmp';
+      fs.writeFileSync(tmp, JSON.stringify({ ...payload, generated_at: new Date().toISOString() }, null, 2));
+      fs.renameSync(tmp, cachePath);
+    } catch (e) { console.warn('[furnish-cache] write:', e.message); }
+    return res.json(payload);
   } catch (e) {
     return res.status(500).json({ ok: false, error: e.message });
   }

← 0f93256 design page: add Share — Pinterest (Pin It) + Instagram (dow  ·  back to Wallco Ai  ·  design page: admin tone-on-tone color-wheel buttons + /api/d e52478f →