// vital-widget — self-updating Scriptable widget // VERSION must stay on this exact line for update detection const VERSION = 1; async function checkForUpdate(currentVersion) { try { const req = new Request("https://clawstin.org/vital-version.txt"); req.timeoutInterval = 4; const remoteVersion = parseInt(await req.loadString()); if (remoteVersion > currentVersion) { const codeReq = new Request("https://clawstin.org/vital-widget-latest.js"); const newCode = await codeReq.loadString(); const fm = FileManager.local(); const path = fm.joinPath(fm.documentsDirectory(), Script.name() + ".js"); fm.writeString(path, newCode); console.log(`vital-widget updated v${currentVersion} → v${remoteVersion}. Reloading next refresh.`); } } catch(e) {} // fail silently, never break the widget } await checkForUpdate(VERSION); // ── Fetch data ────────────────────────────────────────────────────────────── let data; try { const dataReq = new Request("https://clawstin.org/vital.json"); dataReq.timeoutInterval = 6; data = await dataReq.loadJSON(); } catch(e) { // Render error state const w = new ListWidget(); w.backgroundColor = new Color("#0d0d0d"); const errText = w.addText("⚠ No data"); errText.textColor = new Color("#ff4444"); errText.font = Font.boldSystemFont(14); Script.setWidget(w); Script.complete(); return; } // ── Helpers ────────────────────────────────────────────────────────────────── function gaugeColor(pct) { if (pct > 0.6) return new Color("#00e676"); // green if (pct > 0.35) return new Color("#ffeb3b"); // yellow if (pct > 0.15) return new Color("#ff9800"); // orange return new Color("#f44336"); // red } function burnLabel(ratePerHour) { if (ratePerHour < 2) return "LOW"; if (ratePerHour < 6) return "MEDIUM"; return "BURNING"; } // ── Parse data ─────────────────────────────────────────────────────────────── const remaining = data.remaining_balance ?? 0; const total = data.total_balance ?? 1; const rateHour = data.spend_rate_per_hour ?? 0; // $ per hour const pct = Math.min(Math.max(remaining / total, 0), 1); const color = gaugeColor(pct); const burnLbl = burnLabel(rateHour); // ── Build widget ───────────────────────────────────────────────────────────── const widget = new ListWidget(); widget.backgroundColor = new Color("#0d0d0d"); widget.setPadding(16, 18, 16, 18); // — Gauge bar — const gaugeStack = widget.addStack(); gaugeStack.layoutHorizontally(); gaugeStack.size = new Size(0, 10); // height 10, full width const filled = gaugeStack.addStack(); filled.backgroundColor = color; filled.cornerRadius = 4; // Scriptable doesn't do flexbox weights natively, so we use spacing tricks: // Draw filled portion proportionally using a spacer approach via size // We'll draw two stacks side by side; use a fixed 260px total gauge width const GAUGE_W = 260; const filledW = Math.max(4, Math.round(pct * GAUGE_W)); const emptyW = GAUGE_W - filledW; filled.size = new Size(filledW, 10); if (emptyW > 0) { gaugeStack.addSpacer(4); const empty = gaugeStack.addStack(); empty.backgroundColor = new Color("#2a2a2a"); empty.cornerRadius = 4; empty.size = new Size(emptyW, 10); } widget.addSpacer(10); // — Label — const labelText = widget.addText("REMAINING BALANCE"); labelText.textColor = new Color("#888888"); labelText.font = Font.semiboldSystemFont(11); labelText.textOpacity = 0.8; widget.addSpacer(4); // — Dollar amount — const dollarText = widget.addText(`$${remaining.toFixed(2)}`); dollarText.textColor = color; dollarText.font = Font.boldSystemFont(36); widget.addSpacer(10); // — Divider — const divStack = widget.addStack(); divStack.backgroundColor = new Color("#2a2a2a"); divStack.size = new Size(0, 1); widget.addSpacer(10); // — Burn rate row — const burnStack = widget.addStack(); burnStack.layoutHorizontally(); burnStack.centerAlignContent(); const burnBadge = burnStack.addText(burnLbl); burnBadge.textColor = color; burnBadge.font = Font.boldSystemFont(13); burnStack.addSpacer(8); const rateText = burnStack.addText(`$${rateHour.toFixed(2)} spent past hour`); rateText.textColor = new Color("#888888"); rateText.font = Font.systemFont(12); // ── Done ───────────────────────────────────────────────────────────────────── Script.setWidget(widget); if (config.runsInApp) { await widget.presentMedium(); } Script.complete();