← back to AbramsOS
savings: life-optimizer module — cheaper/better substitutes from what you buy (local gemma3:12b, $0)
db2fd4874d8e87cd1de9be49386b73a6f1ce5953 · 2026-07-13 00:19:09 -0700 · Steve
- 0010_savings.sql: savings_suggestion + merchant_coupon tables
- lib/savings-advisor.js: grounded strategy suggestions (no hallucinated SKUs/prices)
- routes/savings.js + views/savings.ejs: dashboard w/ created date+time chips, sort+density, save/dismiss
- seeded reorder items from real Amazon email history; 4 suggestions generated
- switched local model qwen3:14b->gemma3:12b (qwen3 thinking-mode returns empty under format:json)
Files touched
A db/migrations/0010_savings.sqlM lib/ids.jsA lib/savings-advisor.jsA routes/savings.jsM server.jsM views/partials/header.ejsA views/savings.ejs
Diff
commit db2fd4874d8e87cd1de9be49386b73a6f1ce5953
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jul 13 00:19:09 2026 -0700
savings: life-optimizer module — cheaper/better substitutes from what you buy (local gemma3:12b, $0)
- 0010_savings.sql: savings_suggestion + merchant_coupon tables
- lib/savings-advisor.js: grounded strategy suggestions (no hallucinated SKUs/prices)
- routes/savings.js + views/savings.ejs: dashboard w/ created date+time chips, sort+density, save/dismiss
- seeded reorder items from real Amazon email history; 4 suggestions generated
- switched local model qwen3:14b->gemma3:12b (qwen3 thinking-mode returns empty under format:json)
---
db/migrations/0010_savings.sql | 56 ++++++++++++++++
lib/ids.js | 2 +
lib/savings-advisor.js | 141 +++++++++++++++++++++++++++++++++++++++++
routes/savings.js | 82 ++++++++++++++++++++++++
server.js | 2 +
views/partials/header.ejs | 1 +
views/savings.ejs | 139 ++++++++++++++++++++++++++++++++++++++++
7 files changed, 423 insertions(+)
diff --git a/db/migrations/0010_savings.sql b/db/migrations/0010_savings.sql
new file mode 100644
index 0000000..e0febbd
--- /dev/null
+++ b/db/migrations/0010_savings.sql
@@ -0,0 +1,56 @@
+-- 0010_savings.sql
+-- The "life optimizer" layer: turn what Steve actually buys into money-saving /
+-- quality-improving suggestions, plus coupon leads at the places he shops.
+-- savings_suggestion — one actionable idea (cheaper/better substitute, reorder timing,
+-- price drop, or a coupon) generated from purchase + reorder_item data.
+-- merchant_coupon — a discount/coupon lead tied to a merchant Steve buys from.
+-- Both feed the /savings dashboard. Local-LLM ($0) generated; nothing is auto-purchased.
+-- Idempotent. Safe to re-run.
+
+BEGIN;
+
+CREATE TABLE IF NOT EXISTS savings_suggestion (
+ id text PRIMARY KEY,
+ user_id text NOT NULL REFERENCES user_account(id) ON DELETE CASCADE,
+ kind text NOT NULL DEFAULT 'substitute', -- substitute|upgrade|coupon|reorder-timing|price-drop|bundle
+ title text NOT NULL,
+ current_item text, -- what Steve buys today
+ current_price numeric(12,2),
+ suggested_item text, -- the cheaper/better alternative
+ suggested_price numeric(12,2),
+ est_savings numeric(12,2), -- estimated $ saved
+ savings_basis text, -- "per order" | "per year" | "one-time"
+ quality_note text, -- quality tradeoff or upgrade rationale
+ merchant text, -- where to buy the suggestion
+ source_url text, -- link to verify (never auto-buy)
+ rationale text, -- why this suggestion
+ confidence numeric(4,3) NOT NULL DEFAULT 0.5, -- 0..1
+ reorder_item_id text REFERENCES reorder_item(id) ON DELETE SET NULL,
+ status text NOT NULL DEFAULT 'new', -- new|saved|dismissed|done
+ metadata_jsonb jsonb NOT NULL DEFAULT '{}'::jsonb,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS savings_user_status_idx ON savings_suggestion (user_id, status, created_at DESC);
+CREATE INDEX IF NOT EXISTS savings_kind_idx ON savings_suggestion (user_id, kind);
+
+CREATE TABLE IF NOT EXISTS merchant_coupon (
+ id text PRIMARY KEY,
+ user_id text NOT NULL REFERENCES user_account(id) ON DELETE CASCADE,
+ merchant text NOT NULL, -- e.g. "Amazon", "Costco", "Nespresso"
+ merchant_domain text,
+ title text NOT NULL,
+ code text, -- promo code, if any
+ discount_text text, -- "15% off", "$10 off $50"
+ description text,
+ url text,
+ expires_on date,
+ source text, -- where the lead came from
+ status text NOT NULL DEFAULT 'active', -- active|expired|dismissed
+ metadata_jsonb jsonb NOT NULL DEFAULT '{}'::jsonb,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now()
+);
+CREATE INDEX IF NOT EXISTS coupon_user_merchant_idx ON merchant_coupon (user_id, merchant, status);
+
+COMMIT;
diff --git a/lib/ids.js b/lib/ids.js
index d1d8af1..00c751b 100644
--- a/lib/ids.js
+++ b/lib/ids.js
@@ -16,6 +16,8 @@ const PREFIX = {
medication: 'med',
medrecall: 'mrcl',
prescription_fill: 'rxfl',
+ saving: 'sav',
+ coupon: 'coup',
};
function id(kind) {
diff --git a/lib/savings-advisor.js b/lib/savings-advisor.js
new file mode 100644
index 0000000..b235baf
--- /dev/null
+++ b/lib/savings-advisor.js
@@ -0,0 +1,141 @@
+// savings-advisor.js — the "life optimizer" engine.
+//
+// Turns what Steve actually buys (reorder_item + purchase history) into concrete
+// money-saving / quality-improving suggestions, using the LOCAL Ollama model only
+// (Steve's rule: never the Anthropic API; local = $0). Nothing here buys anything —
+// every suggestion is a lead for Steve to review on the /savings dashboard.
+//
+// Design lesson (2026-07-13): asking an LLM for a SPECIFIC cheaper product + exact
+// price makes it hallucinate (qwen3:14b proposed "L'Oréal face cream" for coffee pods).
+// So we ask for a grounded SAVINGS STRATEGY (generic swap / third-party-compatible /
+// bulk / subscribe-save / seasonal timing) + a rough % range — never an invented SKU
+// or a fabricated dollar price. gemma3:12b stays on-topic; qwen3:14b (a thinking model)
+// returns empty under format:json and is unusable here.
+//
+// Honesty guards:
+// • est_savings is derived only when we KNOW the typical price (typical_price * pct);
+// otherwise it stays null and the card shows a "~N% less" range instead of fake $.
+// • source_url is NEVER model-generated (stays null) so nothing looks authoritative
+// that isn't. Real links only come from the coupon feed (merchant_coupon).
+// • confidence is normalized to 0..1 (some models answer 0..100).
+
+const db = require('./db');
+const ollama = require('./ollama');
+const { id } = require('./ids');
+
+const SYSTEM = `You are a frugal, practical household shopping advisor. Given ONE item a
+person buys repeatedly, propose the single best way to spend less on THAT SAME item, or
+a genuine quality upgrade for similar money. Stay strictly on the given item — never
+switch product categories. Never invent a specific brand name or an exact price; speak
+in strategies and rough percentage ranges. Reply as strict JSON only.`;
+
+function prompt(item) {
+ return `Item bought repeatedly:
+- name: ${item.name}
+- category: ${item.category || 'unknown'}
+- unit/size: ${item.unit || 'unknown'}
+- usual merchant: ${item.merchant || 'unknown'}
+- price usually paid: ${item.typical_price != null ? '$' + item.typical_price : 'unknown'}
+
+Return JSON with EXACTLY these keys:
+{
+ "strategy": "generic-swap" | "third-party-compatible" | "bulk-buy" | "subscribe-save" | "seasonal-timing" | "quality-upgrade",
+ "label": "short action label, <= 6 words, about THIS item",
+ "suggestion": "one concrete sentence about this exact item",
+ "est_savings_pct": number, // rough %, 0-70; 0 if it's a quality upgrade
+ "quality_note": "one sentence: tradeoff or why it's better",
+ "merchant": "generic where to look (e.g. Amazon, store brand, warehouse club)",
+ "confidence": number // 0..1 (or 0..100), be conservative
+}`;
+}
+
+function normConf(v) {
+ const n = Number(v);
+ if (!Number.isFinite(n)) return 0.4;
+ const c = n > 1 ? n / 100 : n;
+ return Math.max(0, Math.min(1, c));
+}
+function pct(v) {
+ const n = Number(v);
+ if (!Number.isFinite(n)) return null;
+ return Math.max(0, Math.min(90, n));
+}
+
+async function suggestForItem(userId, item, opts = {}) {
+ let j;
+ try {
+ j = await ollama.generateJson(prompt(item), {
+ system: SYSTEM,
+ timeoutMs: opts.timeoutMs || 60_000,
+ model: opts.model,
+ });
+ } catch (err) {
+ return { ok: false, error: err.message, item: item.name };
+ }
+ const label = j && (j.label || j.suggestion);
+ if (!label) return { ok: false, error: 'empty model output', item: item.name };
+
+ const strategy = String(j.strategy || 'generic-swap');
+ const kind = strategy === 'quality-upgrade' ? 'upgrade' : 'substitute';
+ const savingsPct = pct(j.est_savings_pct);
+ const confidence = normConf(j.confidence);
+ const est_savings =
+ item.typical_price != null && savingsPct != null
+ ? Math.round(Number(item.typical_price) * (savingsPct / 100) * 100) / 100
+ : null;
+
+ // Dedupe: skip if an open suggestion already exists for this item + same strategy.
+ const dupe = await db.query(
+ `SELECT 1 FROM savings_suggestion
+ WHERE user_id = $1 AND reorder_item_id IS NOT DISTINCT FROM $2
+ AND metadata_jsonb->>'strategy' = $3 AND status IN ('new','saved')`,
+ [userId, item.id || null, strategy]
+ );
+ if (dupe.rows.length) return { ok: true, skipped: 'dupe', item: item.name };
+
+ const sid = id('saving');
+ await db.query(
+ `INSERT INTO savings_suggestion
+ (id, user_id, kind, title, current_item, current_price, suggested_item,
+ suggested_price, est_savings, savings_basis, quality_note, merchant,
+ source_url, rationale, confidence, reorder_item_id, metadata_jsonb)
+ VALUES ($1,$2,$3,$4,$5,$6,$7,NULL,$8,$9,$10,$11,NULL,$12,$13,$14,$15)`,
+ [
+ sid, userId, kind,
+ `Save on ${item.name}`.slice(0, 200),
+ item.name, item.typical_price ?? null,
+ String(j.label || strategy).slice(0, 120),
+ est_savings,
+ est_savings != null ? 'per order' : (savingsPct != null ? `~${savingsPct}% less` : 'varies'),
+ (j.quality_note || '').slice(0, 500),
+ (j.merchant || item.merchant || '').slice(0, 120),
+ (j.suggestion || '').slice(0, 1000),
+ confidence, item.id || null,
+ JSON.stringify({ estimate: true, strategy, est_savings_pct: savingsPct, model: opts.model || ollama.DEFAULT_MODEL }),
+ ]
+ );
+ return { ok: true, id: sid, item: item.name, strategy, suggested: j.label };
+}
+
+// Generate suggestions for every active reorder item that doesn't already have a fresh one.
+async function generateSuggestions(userId, opts = {}) {
+ const items = await db.query(
+ `SELECT id, name, merchant, category, unit, typical_price, best_price
+ FROM reorder_item WHERE user_id = $1 AND status = 'active'
+ ORDER BY updated_at DESC LIMIT $2`,
+ [userId, opts.limit || 50]
+ );
+ const results = [];
+ for (const item of items.rows) {
+ results.push(await suggestForItem(userId, item, opts));
+ }
+ return {
+ scanned: items.rows.length,
+ created: results.filter((r) => r.ok && r.id).length,
+ skipped: results.filter((r) => r.skipped).length,
+ errors: results.filter((r) => !r.ok),
+ results,
+ };
+}
+
+module.exports = { generateSuggestions, suggestForItem };
diff --git a/routes/savings.js b/routes/savings.js
new file mode 100644
index 0000000..f83ba60
--- /dev/null
+++ b/routes/savings.js
@@ -0,0 +1,82 @@
+// Savings — the life-optimizer dashboard: cheaper/better substitutes for what Steve
+// buys, plus coupon leads at the merchants he shops. Read + curate; never auto-buys.
+const express = require('express');
+const db = require('../lib/db');
+const advisor = require('../lib/savings-advisor');
+const audit = require('../lib/audit');
+
+const router = express.Router();
+const DEV_USER_ID = 'user_steve';
+
+async function listSuggestions(userId) {
+ const r = await db.query(
+ `SELECT id, kind, title, current_item, current_price, suggested_item, suggested_price,
+ est_savings, savings_basis, quality_note, merchant, source_url, rationale,
+ confidence, status, created_at
+ FROM savings_suggestion
+ WHERE user_id = $1 AND status IN ('new','saved')
+ ORDER BY (status='saved') DESC, est_savings DESC NULLS LAST, confidence DESC
+ LIMIT 200`,
+ [userId]
+ );
+ return r.rows;
+}
+
+async function listCoupons(userId) {
+ const r = await db.query(
+ `SELECT id, merchant, merchant_domain, title, code, discount_text, description,
+ url, expires_on, source, created_at
+ FROM merchant_coupon
+ WHERE user_id = $1 AND status = 'active'
+ AND (expires_on IS NULL OR expires_on >= current_date)
+ ORDER BY created_at DESC LIMIT 200`,
+ [userId]
+ );
+ return r.rows;
+}
+
+router.get('/savings', async (_req, res) => {
+ const [suggestions, coupons] = await Promise.all([
+ listSuggestions(DEV_USER_ID),
+ listCoupons(DEV_USER_ID),
+ ]);
+ const totalSavings = suggestions.reduce((s, x) => s + (Number(x.est_savings) || 0), 0);
+ res.render('savings', { suggestions, coupons, totalSavings });
+});
+
+router.get('/api/savings', async (_req, res) => {
+ res.json(await listSuggestions(DEV_USER_ID));
+});
+
+router.get('/api/coupons', async (_req, res) => {
+ res.json(await listCoupons(DEV_USER_ID));
+});
+
+// Generate fresh substitute suggestions from reorder_item data (local LLM, $0).
+router.post('/api/savings/run', async (_req, res) => {
+ try {
+ const out = await advisor.generateSuggestions(DEV_USER_ID, { limit: 50 });
+ await audit.log({
+ actorType: 'system', actorId: 'savings-advisor', objectType: 'savings_suggestion',
+ objectId: null, eventType: 'savings_generated',
+ metadata: { scanned: out.scanned, created: out.created },
+ }).catch(() => {});
+ res.json({ ok: true, ...out, results: undefined });
+ } catch (err) {
+ res.status(500).json({ ok: false, error: err.message });
+ }
+});
+
+router.post('/api/savings/:id/status', async (req, res) => {
+ const status = ['new', 'saved', 'dismissed', 'done'].includes(req.body.status) ? req.body.status : null;
+ if (!status) return res.status(400).json({ error: 'bad status' });
+ const r = await db.query(
+ `UPDATE savings_suggestion SET status = $1, updated_at = now()
+ WHERE id = $2 AND user_id = $3 RETURNING id, status`,
+ [status, req.params.id, DEV_USER_ID]
+ );
+ if (!r.rows.length) return res.status(404).json({ error: 'not found' });
+ res.json(r.rows[0]);
+});
+
+module.exports = router;
diff --git a/server.js b/server.js
index 7ec6b9e..2075403 100644
--- a/server.js
+++ b/server.js
@@ -24,6 +24,7 @@ const recallsRouter = require('./routes/recalls');
const uploadRouter = require('./routes/upload');
const billsRouter = require('./routes/bills');
const reordersRouter = require('./routes/reorders');
+const savingsRouter = require('./routes/savings');
const warrantiesRouter = require('./routes/warranties');
const peopleRouter = require('./routes/people');
const medicationsRouter = require('./routes/medications');
@@ -87,6 +88,7 @@ app.use(recallsRouter); // /recalls, /api/recalls
app.use(uploadRouter); // /api/upload/csv
app.use(billsRouter); // /bills, /api/bills*
app.use(reordersRouter); // /reorders, /api/reorders*
+app.use(savingsRouter); // /savings, /api/savings*, /api/coupons
app.use(warrantiesRouter); // /warranties, /api/warranties*
app.use(peopleRouter); // /household, /api/people*
app.use(medicationsRouter); // /medications, /api/medications*
diff --git a/views/partials/header.ejs b/views/partials/header.ejs
index 3a4a795..fd673f8 100644
--- a/views/partials/header.ejs
+++ b/views/partials/header.ejs
@@ -26,6 +26,7 @@
<a href="/bills">Bills</a>
<a href="/warranties">Warranties</a>
<a href="/reorders">Reorders</a>
+ <a href="/savings">Savings</a>
<a href="/medications">Meds</a>
<a href="/prescriptions">Rx</a>
<a href="/household">Household</a>
diff --git a/views/savings.ejs b/views/savings.ejs
new file mode 100644
index 0000000..9f44896
--- /dev/null
+++ b/views/savings.ejs
@@ -0,0 +1,139 @@
+<%- include('partials/header', { title: 'Savings' }) %>
+
+<section class="page-head">
+ <div>
+ <h1>Savings & Smart Buys</h1>
+ <p class="subtle">
+ <%= suggestions.length %> ideas ·
+ <% if (totalSavings) { %>est. USD <%= totalSavings.toFixed(2) %> potential savings<% } else { %>run the advisor to find cheaper & better swaps<% } %>
+ · <span class="subtle">prices are AI estimates — verify before buying</span>
+ </p>
+ </div>
+ <div class="grid-controls">
+ <label>Sort
+ <select data-sort-for="savings">
+ <option value="savings-desc">Biggest savings</option>
+ <option value="confidence-desc">Most confident</option>
+ <option value="created-desc">Newest</option>
+ <option value="merchant-asc">Merchant A→Z</option>
+ </select>
+ </label>
+ <label>Density
+ <input data-density-for="savings" type="range" min="260" max="480" step="10" value="340">
+ </label>
+ <button type="button" class="btn primary" id="runAdvisor">Find savings</button>
+ </div>
+</section>
+
+<% if (!suggestions.length) { %>
+ <section class="empty glass">
+ <p>No suggestions yet. Click <strong>Find savings</strong> — the advisor reads what you
+ buy a lot (your <a href="/reorders">Reorders</a>) and proposes cheaper or better swaps,
+ using a local AI model ($0).</p>
+ </section>
+<% } %>
+
+<section data-grid="savings" class="purchase-grid" style="grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));">
+ <% suggestions.forEach(s => {
+ const conf = Math.round((Number(s.confidence)||0) * 100);
+ const created = new Date(s.created_at);
+ const createdStr = created.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
+ %>
+ <article class="purchase glass<%= s.status==='saved' ? ' saved' : '' %>"
+ data-id="<%= s.id %>"
+ data-merchant="<%= (s.merchant||'').toLowerCase() %>"
+ data-savings="<%= Number(s.est_savings)||0 %>"
+ data-confidence="<%= Number(s.confidence)||0 %>"
+ data-created="<%= created.toISOString() %>">
+ <header>
+ <h3><%= s.suggested_item %></h3>
+ <span class="chip <%= s.kind==='upgrade' ? 'upgrade' : 'save' %>"><%= s.kind %></span>
+ </header>
+
+ <div class="amount">
+ <% if (s.est_savings && Number(s.est_savings) > 0) { %>
+ <span class="save-amt">save ~$<%= Number(s.est_savings).toFixed(2) %></span>
+ <span class="subtle"><%= s.savings_basis || 'per order' %></span>
+ <% } else if (s.savings_basis && /%/.test(s.savings_basis)) { %>
+ <span class="save-amt"><%= s.savings_basis %></span>
+ <% } else { %><span class="subtle">smart buy</span><% } %>
+ </div>
+
+ <dl class="meta">
+ <% if (s.current_item) { %>
+ <dt>Instead of</dt>
+ <dd><%= s.current_item %><% if (s.current_price) { %> <span class="subtle">($<%= Number(s.current_price).toFixed(2) %>)</span><% } %></dd>
+ <% } %>
+ <% if (s.merchant) { %><dt>Where</dt><dd><%= s.merchant %></dd><% } %>
+ <% if (s.quality_note) { %><dt>Quality</dt><dd><%= s.quality_note %></dd><% } %>
+ <dt>Confidence</dt><dd><span class="badge<%= conf>=70?' amber':'' %>"><%= conf %>%</span></dd>
+ </dl>
+
+ <% if (s.rationale) { %><p class="rationale subtle"><%= s.rationale %></p><% } %>
+
+ <footer class="card-foot">
+ <span class="when" title="<%= created.toISOString() %>">🕓 <%= createdStr %></span>
+ <span class="actions">
+ <% if (s.status !== 'saved') { %><button type="button" class="btn tiny" data-act="save" data-id="<%= s.id %>">Save</button><% } %>
+ <button type="button" class="btn tiny ghost" data-act="dismiss" data-id="<%= s.id %>">Dismiss</button>
+ </span>
+ </footer>
+ </article>
+ <% }) %>
+</section>
+
+<% if (coupons.length) { %>
+ <section class="page-head" style="margin-top:2rem"><div><h2>Coupons at places you shop</h2></div></section>
+ <section class="purchase-grid" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));">
+ <% coupons.forEach(c => {
+ const cCreated = new Date(c.created_at);
+ const cStr = cCreated.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
+ %>
+ <article class="purchase glass">
+ <header><h3><%= c.merchant %></h3><% if (c.discount_text) { %><span class="chip save"><%= c.discount_text %></span><% } %></header>
+ <div class="amount"><%= c.title %></div>
+ <dl class="meta">
+ <% if (c.code) { %><dt>Code</dt><dd><code><%= c.code %></code></dd><% } %>
+ <% if (c.expires_on) { %><dt>Expires</dt><dd><%= c.expires_on %></dd><% } %>
+ <% if (c.source) { %><dt>Source</dt><dd class="subtle"><%= c.source %></dd><% } %>
+ </dl>
+ <footer class="card-foot">
+ <span class="when" title="<%= cCreated.toISOString() %>">🕓 <%= cStr %></span>
+ <% if (c.url) { %><a class="btn tiny" href="<%= c.url %>" target="_blank" rel="noopener noreferrer">Open</a><% } %>
+ </footer>
+ </article>
+ <% }) %>
+ </section>
+<% } %>
+
+<script>
+ window.__csrf = '<%= csrfToken %>';
+ async function post(url, body) {
+ const r = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(Object.assign({ _csrf: window.__csrf }, body || {})),
+ });
+ return r.json();
+ }
+ document.getElementById('runAdvisor')?.addEventListener('click', async (e) => {
+ const btn = e.currentTarget; btn.disabled = true; btn.textContent = 'Thinking…';
+ try {
+ const out = await post('/api/savings/run');
+ btn.textContent = out.ok ? `+${out.created} found` : 'Error';
+ if (out.ok && out.created) setTimeout(() => location.reload(), 700);
+ else btn.disabled = false;
+ } catch (_) { btn.textContent = 'Error'; btn.disabled = false; }
+ });
+ document.querySelectorAll('[data-act]').forEach(b => b.addEventListener('click', async (e) => {
+ const el = e.currentTarget; const id = el.dataset.id;
+ const status = el.dataset.act === 'save' ? 'saved' : 'dismissed';
+ el.disabled = true;
+ await post('/api/savings/' + id + '/status', { status });
+ const card = el.closest('article');
+ if (status === 'dismissed') { card.style.opacity = 0.35; card.remove(); }
+ else { card.classList.add('saved'); el.remove(); }
+ }));
+</script>
+
+<%- include('partials/footer', { gridScript: true }) %>
← 367d888 auto-save: 2026-07-12T23:51:20 (1 files) — backups/
·
back to AbramsOS
·
savings: home dashboard tile + honest merchant-savings leads a121be0 →