diff --git a/src/frontend/app.js b/src/frontend/app.js
index eee3953..030bead 100644
--- a/src/frontend/app.js
+++ b/src/frontend/app.js
@@ -100,6 +100,34 @@ function estimateCost(fileSize) {
return tokens * 0.3 * (3.0 / 1e6) + tokens * 0.7 * (15.0 / 1e6);
}
+// ── Subscription config helpers ──────────────────────────────────
+function getSubscriptionConfig() {
+ var raw = JSON.parse(localStorage.getItem('codedash-subscription') || 'null');
+ if (!raw) return { entries: [] };
+ // Migrate old single-entry format {plan, paid} → new multi-period {entries: [...]}
+ if (!raw.entries) return { entries: [{ plan: raw.plan || 'Subscription', paid: raw.paid || 0, from: '' }] };
+ return raw;
+}
+function saveSubscriptionConfig(cfg) { localStorage.setItem('codedash-subscription', JSON.stringify(cfg)); }
+function subTotalPaid(entries) { return entries.reduce(function(s,e){return s+(parseFloat(e.paid)||0);},0); }
+function addSubEntry() {
+ var plan = (document.getElementById('sub-new-plan').value || '').trim();
+ var paid = parseFloat(document.getElementById('sub-new-paid').value) || 0;
+ var from = (document.getElementById('sub-new-from').value || '').trim();
+ if (!paid) return;
+ var cfg = getSubscriptionConfig();
+ cfg.entries.push({ plan: plan || 'Subscription', paid: paid, from: from });
+ cfg.entries.sort(function(a,b){return (a.from||'').localeCompare(b.from||'');});
+ saveSubscriptionConfig(cfg);
+ render();
+}
+function removeSubEntry(idx) {
+ var cfg = getSubscriptionConfig();
+ cfg.entries.splice(idx, 1);
+ saveSubscriptionConfig(cfg);
+ render();
+}
+
async function loadRealCost(sessionId, project) {
try {
var resp = await fetch('/api/cost/' + sessionId + '?project=' + encodeURIComponent(project));
@@ -1881,6 +1909,56 @@ async function renderAnalytics(container) {
html += '';
}
+ // ── Subscription vs API ────────────────────────────────────
+ var sub = getSubscriptionConfig();
+ var subEntries = (sub && sub.entries) || [];
+ var totalPaid = subTotalPaid(subEntries);
+ html += '
';
+ html += '
Subscription vs API
';
+
+ if (totalPaid > 0) {
+ var savings = data.totalCost - totalPaid;
+ var multiplier = data.totalCost / totalPaid;
+ var savingsPositive = savings > 0;
+ var breakdown = subEntries.map(function(e) {
+ return escHtml(e.plan || 'Sub') + ' $' + parseFloat(e.paid).toFixed(0);
+ }).join(' + ');
+ html += '
';
+ html += '
$' + totalPaid.toFixed(2) + 'Paid (' + breakdown + ')
';
+ html += '
$' + data.totalCost.toFixed(2) + 'Would cost at API rates
';
+ html += '
' + (savingsPositive ? '+' : '') + '$' + Math.abs(savings).toFixed(2) + '' + (savingsPositive ? 'Saved (' + multiplier.toFixed(1) + '\u00d7 ROI)' : 'API would be cheaper') + '
';
+ html += '
';
+ var barPct = Math.min(100, data.totalCost > 0 ? (totalPaid / data.totalCost * 100) : 100);
+ html += '
';
+ html += '
';
+ html += '
';
+ } else {
+ html += '
Add your subscription periods below to see how much you\'re saving vs API rates.
';
+ }
+
+ // Period list
+ html += '
';
+ if (subEntries.length > 0) {
+ subEntries.forEach(function(e, i) {
+ html += '
';
+ html += '' + escHtml(e.plan || '\u2014') + '';
+ html += '$' + parseFloat(e.paid || 0).toFixed(2) + '';
+ html += '' + (e.from ? 'from ' + e.from : 'no date') + '';
+ html += '';
+ html += '
';
+ });
+ }
+ html += '
';
+
+ // Add form
+ html += '
';
+ html += '';
+ html += '';
+ html += '';
+ html += '';
+ html += '
';
+ html += '
';
+
// ── Daily cost chart ───────────────────────────────────────
var dayKeys = Object.keys(data.byDay).sort();
var last30 = dayKeys.slice(-30);
diff --git a/src/frontend/styles.css b/src/frontend/styles.css
index 50a136c..f2df9e1 100644
--- a/src/frontend/styles.css
+++ b/src/frontend/styles.css
@@ -2116,6 +2116,112 @@ body {
.token-cache-create { border-color: rgba(251, 191, 36, 0.3); }
.token-context { border-color: rgba(168, 85, 247, 0.3); }
+/* ── Subscription vs API ──────────────────────────────────── */
+
+.subscription-section { margin-top: 8px; }
+
+.sub-comparison {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 10px;
+ margin-bottom: 12px;
+}
+
+.sub-card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 14px;
+ text-align: center;
+}
+
+.sub-val { font-size: 20px; font-weight: 700; display: block; }
+.sub-label { font-size: 11px; color: var(--text-muted); display: block; margin-top: 4px; }
+
+.sub-paid .sub-val { color: var(--text); }
+.sub-api .sub-val { color: var(--accent-blue, #60a5fa); }
+.sub-savings .sub-val { color: var(--accent-green); }
+.sub-loss .sub-val { color: var(--accent-red, #f87171); }
+
+.sub-bar-track {
+ height: 8px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ margin-bottom: 14px;
+ overflow: hidden;
+}
+
+.sub-bar-fill {
+ height: 100%;
+ background: var(--accent-green);
+ border-radius: 4px;
+ transition: width 0.3s;
+}
+
+.sub-hint {
+ color: var(--text-muted);
+ font-size: 13px;
+ margin: 8px 0;
+}
+
+.sub-entries { margin-bottom: 10px; }
+
+.sub-entry-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 6px 0;
+ border-bottom: 1px solid var(--border);
+ font-size: 13px;
+}
+
+.sub-entry-plan { font-weight: 600; min-width: 80px; }
+.sub-entry-paid { color: var(--accent-green); min-width: 70px; }
+.sub-entry-from { color: var(--text-muted); flex: 1; }
+.sub-entry-remove {
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ cursor: pointer;
+ font-size: 16px;
+ padding: 2px 6px;
+ border-radius: 4px;
+}
+.sub-entry-remove:hover { background: rgba(248, 113, 113, 0.15); color: var(--accent-red, #f87171); }
+
+.sub-add-form {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+
+.sub-add-form input {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 6px 10px;
+ color: var(--text);
+ font-size: 13px;
+}
+
+.sub-add-form input[type="text"] { width: 130px; }
+.sub-add-form input[type="number"] { width: 100px; }
+.sub-add-form input[type="date"] { width: 140px; }
+
+.sub-add-form button {
+ background: var(--accent-blue, #60a5fa);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 14px;
+ cursor: pointer;
+ font-size: 13px;
+ font-weight: 600;
+}
+.sub-add-form button:hover { opacity: 0.85; }
+
/* ── Update banner ──────────────────────────────────────────── */
.update-banner {