App._effPeriod = '7d';
App.renderEfficiency = function() {
var self = this;
var period = this._effPeriod || '7d';
var effTab = this._effTab || 'performance';
var tabBar = '<div class="tabs" style="margin-bottom:1rem">'
+ '<button data-eff-tab="performance"' + (effTab === 'performance' ? ' class="active"' : '') + '>Performance</button>'
+ '<button data-eff-tab="recommendations"' + (effTab === 'recommendations' ? ' class="active"' : '') + '>Recommendations</button>'
+ '</div>';
if (effTab === 'recommendations') {
var quickTips = self._quickTipsHtml || '';
return this.renderRecommendations().then(function(recommendationsHtml) {
return tabBar + quickTips + '<div style="margin-top:1.5rem"></div>' + recommendationsHtml;
});
}
return Promise.all([
api('/api/stats/efficiency?period=' + encodeURIComponent(period)),
api('/api/models/selections?limit=80').catch(function() { return { events: [] }; }),
api('/api/models/routing-diagnostics').catch(function() { return { config: {} }; }),
api('/api/config').catch(function() { return {}; })
]).then(function(arr) {
var report = arr[0] || {};
var modelSelections = (arr[1] && arr[1].events) ? arr[1].events : [];
var configData = arr[3] || {};
var persistedRoutingConfig = (configData && configData.models && configData.models.routing) ? configData.models.routing : {};
var routingConfig = Object.assign({}, (arr[2] && arr[2].config) ? arr[2].config : {}, persistedRoutingConfig);
var routingProfile = deriveRoutingProfile(routingConfig);
App._routingProfileDefaults = normalizeRoutingProfile(routingProfile);
if (!App._routingProfileDraft) App._routingProfileDraft = normalizeRoutingProfile({ correctness: routingProfile.correctness, cost: routingProfile.cost, speed: routingProfile.speed });
App._routingProfileDraft = normalizeRoutingProfile(App._routingProfileDraft);
var models = report.models || {};
var ts = report.time_series || [];
var totals = report.totals || {};
var modelNames = Object.keys(models).sort(function(a, b) {
return (models[b].cost.total || 0) - (models[a].cost.total || 0);
});
var spider = renderRoutingSpiderSvg(App._routingProfileDraft || routingProfile);
var persistedLabel = '';
if (App._routingProfilePersistedAt) {
var savedAt = new Date(App._routingProfilePersistedAt);
var savedText = Number.isNaN(savedAt.getTime()) ? App._routingProfilePersistedAt : savedAt.toLocaleString();
persistedLabel = '<div style="margin-top:0.45rem;font-size:0.6875rem;color:var(--muted)">'
+ (App._routingProfilePersisted ? 'Saved to config: ' : 'Applied: ')
+ esc(savedText)
+ '</div>';
}
var profileCard = '<div class="card" style="margin-bottom:1rem;padding-bottom:0.75rem"><div class="card-title">Routing Weights (Correctness / Cost / Speed)</div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.65rem">Operator profile mapped onto routing controls. Adjust and apply to persist to runtime config.</div>'
+ '<div class="routing-profile-grid">'
+ '<div id="routing-profile-spider-host">' + spider + '</div>'
+ '<div>'
+ '<div class="routing-slider-row"><label for="routing-slider-correctness"><span>Correctness</span><span id="routing-slider-correctness-val">' + round2((App._routingProfileDraft || routingProfile).correctness).toFixed(2) + '</span></label><input id="routing-slider-correctness" data-routing-slider="correctness" type="range" min="0" max="1" step="0.01" value="' + round2((App._routingProfileDraft || routingProfile).correctness).toFixed(2) + '"></div>'
+ '<div class="routing-slider-row"><label for="routing-slider-cost"><span>Cost</span><span id="routing-slider-cost-val">' + round2((App._routingProfileDraft || routingProfile).cost).toFixed(2) + '</span></label><input id="routing-slider-cost" data-routing-slider="cost" type="range" min="0" max="1" step="0.01" value="' + round2((App._routingProfileDraft || routingProfile).cost).toFixed(2) + '"></div>'
+ '<div class="routing-slider-row"><label for="routing-slider-speed"><span>Speed</span><span id="routing-slider-speed-val">' + round2((App._routingProfileDraft || routingProfile).speed).toFixed(2) + '</span></label><input id="routing-slider-speed" data-routing-slider="speed" type="range" min="0" max="1" step="0.01" value="' + round2((App._routingProfileDraft || routingProfile).speed).toFixed(2) + '"></div>'
+ '<div style="font-size:0.72rem;color:var(--muted);margin-top:0.35rem">Total: <span id="routing-slider-total">' + routingProfileTotal((App._routingProfileDraft || routingProfile)).toFixed(2) + '</span> / 1.00</div>'
+ '<div class="routing-profile-actions"><button class="btn" id="routing-profile-apply" style="font-size:0.6875rem;padding:0.3rem 0.7rem">Apply profile</button><button class="btn secondary" id="routing-profile-reset" style="font-size:0.6875rem;padding:0.3rem 0.7rem">Reset</button></div>'
+ persistedLabel
+ '</div></div></div>';
// Context budget card
App._contextBudgetDefaults = normalizeContextBudget((configData && configData.context_budget) || {});
if (!App._contextBudgetDraft) App._contextBudgetDraft = normalizeContextBudget(App._contextBudgetDefaults);
App._contextBudgetDraft = normalizeContextBudget(App._contextBudgetDraft);
var budgetCfg = App._contextBudgetDraft;
var bL0 = budgetCfg.l0, bL1 = budgetCfg.l1, bL2 = budgetCfg.l2, bL3 = budgetCfg.l3;
var chMin = budgetCfg.channel_minimum;
var budgetPersistedLabel = '';
if (App._contextBudgetPersistedAt) {
var budgetSavedAt = new Date(App._contextBudgetPersistedAt);
var budgetSavedText = Number.isNaN(budgetSavedAt.getTime()) ? App._contextBudgetPersistedAt : budgetSavedAt.toLocaleString();
budgetPersistedLabel = '<div style="margin-top:0.45rem;font-size:0.6875rem;color:var(--muted)">'
+ (App._contextBudgetPersisted ? 'Saved to config: ' : 'Applied: ')
+ esc(budgetSavedText)
+ '</div>';
}
var budgetCard = '<div class="card" style="margin-bottom:1rem;padding-bottom:0.75rem"><div class="card-title">Context Budget (Tokens per Complexity Level)</div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.65rem">Controls how many tokens the agent assembles per turn at each complexity level.</div>'
+ '<div class="routing-slider-row"><label for="budget-l0"><span>L0 (Trivial)</span><span id="budget-l0-val">' + bL0 + '</span></label><input id="budget-l0" data-budget-slider="l0" type="range" min="1000" max="16000" step="500" value="' + bL0 + '"></div>'
+ '<div class="routing-slider-row"><label for="budget-l1"><span>L1 (Low)</span><span id="budget-l1-val">' + bL1 + '</span></label><input id="budget-l1" data-budget-slider="l1" type="range" min="2000" max="32000" step="500" value="' + bL1 + '"></div>'
+ '<div class="routing-slider-row"><label for="budget-l2"><span>L2 (Moderate)</span><span id="budget-l2-val">' + bL2 + '</span></label><input id="budget-l2" data-budget-slider="l2" type="range" min="4000" max="64000" step="1000" value="' + bL2 + '"></div>'
+ '<div class="routing-slider-row"><label for="budget-l3"><span>L3 (High)</span><span id="budget-l3-val">' + bL3 + '</span></label><input id="budget-l3" data-budget-slider="l3" type="range" min="8000" max="128000" step="1000" value="' + bL3 + '"></div>'
+ '<div class="settings-row" style="margin-top:0.5rem"><div class="settings-label" style="font-size:0.72rem">Channel Minimum</div><select id="budget-channel-min" style="font-size:0.72rem;padding:0.25rem 0.5rem;border:1px solid var(--border);border-radius:4px;background:var(--surface);color:var(--text)"><option value="L0"' + (chMin === 'L0' ? ' selected' : '') + '>L0</option><option value="L1"' + (chMin === 'L1' ? ' selected' : '') + '>L1</option><option value="L2"' + (chMin === 'L2' ? ' selected' : '') + '>L2</option><option value="L3"' + (chMin === 'L3' ? ' selected' : '') + '>L3</option></select></div>'
+ '<div class="routing-profile-actions"><button class="btn" id="budget-apply" style="font-size:0.6875rem;padding:0.3rem 0.7rem">Apply budget</button><button class="btn secondary" id="budget-reset" style="font-size:0.6875rem;padding:0.3rem 0.7rem">Reset</button></div>'
+ budgetPersistedLabel
+ '</div>';
var decisionGraph = renderModelDecisionGraph(modelSelections, App._modelGraphFocusTurn, App._modelGraphFocusModel, App._modelGraphFocusEdge);
// Period selector
var periods = ['1h', '24h', '7d', '30d', 'all'];
var periodBar = '<div style="display:flex;gap:0.5rem;margin-bottom:1.25rem;flex-wrap:wrap">';
periods.forEach(function(p) {
var cls = p === period ? 'background:var(--accent);color:var(--bg)' : 'background:var(--surface);color:var(--muted);border:1px solid var(--border)';
periodBar += '<button data-eff-period="' + p + '" style="padding:0.375rem 0.875rem;border-radius:4px;font-family:var(--font);font-size:0.8125rem;cursor:pointer;border:none;' + cls + '">' + p.toUpperCase() + '</button>';
});
periodBar += '</div>';
// Cost overview banner
var banner = '<div class="card" style="margin-bottom:1rem;background:linear-gradient(135deg,var(--surface) 0%,rgba(193,128,255,0.08) 100%)">'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem">'
+ '<div><div style="font-size:0.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.25rem">Total Cost</div><div style="font-size:1.5rem;font-weight:700;color:var(--accent)">$' + (totals.total_cost || 0).toFixed(4) + '</div></div>'
+ '<div><div style="font-size:0.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.25rem">Cache Savings</div><div style="font-size:1.5rem;font-weight:700;color:var(--success)">$' + (totals.total_cache_savings || 0).toFixed(4) + '</div></div>'
+ '<div><div style="font-size:0.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.25rem">Total Turns</div><div style="font-size:1.5rem;font-weight:700">' + (totals.total_turns || 0).toLocaleString() + '</div></div>'
+ '<div><div style="font-size:0.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.25rem">Models Active</div><div style="font-size:1.5rem;font-weight:700">' + modelNames.length + '</div></div>'
+ '</div></div>';
// Model comparison cards
var trendArrow = function(t) {
if (t === 'increasing') return '<span style="color:var(--error)">▲</span>';
if (t === 'decreasing') return '<span style="color:var(--success)">▼</span>';
return '<span style="color:var(--muted)">▬</span>';
};
var trendArrowGood = function(t) {
if (t === 'increasing') return '<span style="color:var(--success)">▲</span>';
if (t === 'decreasing') return '<span style="color:var(--error)">▼</span>';
return '<span style="color:var(--muted)">▬</span>';
};
var metricHelp = {
'output_density': 'Output tokens per input token. Higher = more content per context. Range: 0.3-0.8.',
'cache_hit_rate': 'Percentage served from cache. Target: > 30%.',
'cost_per_turn': 'Average cost per conversation turn.',
'budget_utilization': 'How much of allocated context budget is used.'
};
var modelColors = ['#c180ff', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#14b8a6'];
var cards = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:1rem;margin-bottom:1.25rem">';
modelNames.forEach(function(name, idx) {
var m = models[name];
var c = m.cost || {};
var t = m.trend || {};
var color = modelColors[idx % modelColors.length];
var isMostExpensive = totals.most_expensive_model === name;
var isMostEfficient = totals.most_efficient_model === name;
var badges = '';
if (isMostExpensive) badges += '<span style="background:rgba(239,68,68,0.15);color:var(--error);padding:0.125rem 0.5rem;border-radius:3px;font-size:0.6875rem;margin-right:0.25rem">HIGHEST COST</span>';
if (isMostEfficient) badges += '<span style="background:rgba(34,197,94,0.15);color:var(--success);padding:0.125rem 0.5rem;border-radius:3px;font-size:0.6875rem">MOST EFFICIENT</span>';
var lastInvoke = m.last_invoked_at || null;
var isRecent = lastInvoke && (Date.now() - new Date(lastInvoke).getTime()) < 24 * 60 * 60 * 1000;
var hasErrors = (m.error_count || 0) > (m.success_count || 0) * 0.1;
var healthBadge = '';
if (!lastInvoke) {
healthBadge = '<span class="badge muted">Not tested</span>';
} else if (hasErrors) {
healthBadge = '<span class="badge error">Degraded</span>';
} else if (isRecent) {
healthBadge = '<span class="badge success">Healthy</span>';
} else {
healthBadge = '<span class="badge muted">Idle</span>';
}
cards += '<div class="card" style="border-left:3px solid ' + color + '">'
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.75rem"><div style="font-weight:600;font-size:0.9375rem">' + esc(name) + '</div><div>' + healthBadge + ' ' + badges + '</div></div>'
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:0.5rem;font-size:0.8125rem">'
+ '<div><span style="color:var(--muted)">Turns</span><div style="font-weight:600">' + m.total_turns.toLocaleString() + '</div></div>'
+ '<div><span style="color:var(--muted)">Total Cost</span><div style="font-weight:600;color:' + color + '">$' + c.total.toFixed(4) + '</div></div>'
+ '<div><span style="color:var(--muted)" title="' + metricHelp['output_density'] + '">Output Density</span><div style="font-weight:600">' + m.avg_output_density.toFixed(3) + ' ' + trendArrowGood(t.output_density) + '</div></div>'
+ '<div><span style="color:var(--muted)" title="' + metricHelp['cost_per_turn'] + '">Cost/Turn</span><div style="font-weight:600">$' + c.effective_per_turn.toFixed(4) + ' ' + trendArrow(t.cost_per_turn) + '</div></div>'
+ '<div><span style="color:var(--muted)" title="' + metricHelp['cache_hit_rate'] + '">Cache Hit Rate</span><div style="font-weight:600">' + (m.cache_hit_rate * 100).toFixed(1) + '% ' + trendArrowGood(t.cache_hit_rate) + '</div></div>'
+ '<div><span style="color:var(--muted)">Cache Savings</span><div style="font-weight:600;color:var(--success)">$' + c.cache_savings.toFixed(4) + '</div></div>'
+ '<div><span style="color:var(--muted)">$/Output Token</span><div style="font-weight:600">' + (c.per_output_token * 1000).toFixed(4) + '/1k</div></div>'
+ '<div><span style="color:var(--muted)">Cost Trend</span><div style="font-weight:600">' + trendArrow(c.cumulative_trend) + ' ' + esc(c.cumulative_trend) + '</div></div>'
+ '</div>';
// Cost attribution bar
var attr = c.attribution || {};
var attrBar = '<div style="margin-top:0.75rem"><div style="font-size:0.6875rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.375rem">Input Cost Attribution</div>';
var sys = attr.system_prompt || {};
var mem = attr.memories || {};
var hist = attr.history || {};
if (sys.pct > 0 || mem.pct > 0 || hist.pct > 0) {
attrBar += '<div style="display:flex;height:8px;border-radius:4px;overflow:hidden;margin-bottom:0.375rem">';
if (sys.pct > 0) attrBar += '<div style="width:' + sys.pct + '%;background:#c180ff" title="System prompt: ' + sys.pct.toFixed(1) + '%"></div>';
if (mem.pct > 0) attrBar += '<div style="width:' + mem.pct + '%;background:#22c55e" title="Memories: ' + mem.pct.toFixed(1) + '%"></div>';
if (hist.pct > 0) attrBar += '<div style="width:' + hist.pct + '%;background:#f59e0b" title="History: ' + hist.pct.toFixed(1) + '%"></div>';
attrBar += '</div>';
attrBar += '<div style="display:flex;gap:0.75rem;font-size:0.6875rem;color:var(--muted)">';
if (sys.pct > 0) attrBar += '<span style="color:#c180ff">\u25cf System ' + sys.pct.toFixed(0) + '%</span>';
if (mem.pct > 0) attrBar += '<span style="color:#22c55e">\u25cf Memory ' + mem.pct.toFixed(0) + '%</span>';
if (hist.pct > 0) attrBar += '<span style="color:#f59e0b">\u25cf History ' + hist.pct.toFixed(0) + '%</span>';
attrBar += '</div>';
} else {
attrBar += '<div style="font-size:0.75rem;color:var(--muted);font-style:italic">No context snapshot data available</div>';
}
attrBar += '</div>';
cards += attrBar + '</div>';
});
cards += '</div>';
if (modelNames.length === 0) {
cards = '<div class="card" style="text-align:center;padding:2rem;color:var(--muted)"><div style="font-size:1.5rem;margin-bottom:0.5rem">No inference data</div><div style="font-size:0.875rem">Run some inference requests to see efficiency metrics here.</div></div>';
}
// Time series chart (CSS bar chart)
var tsChart = '';
if (ts.length > 0) {
var maxCost = Math.max.apply(null, ts.map(function(p) { return p.cost; }));
if (maxCost === 0) maxCost = 1;
tsChart = '<div class="card" style="margin-bottom:1.25rem"><div style="font-weight:600;margin-bottom:0.75rem">Cost Over Time</div>';
tsChart += '<div style="display:flex;align-items:flex-end;gap:2px;height:120px;padding-bottom:1.5rem;position:relative">';
var buckets = [];
ts.forEach(function(p) { if (buckets.indexOf(p.bucket) === -1) buckets.push(p.bucket); });
var bucketTotals = {};
ts.forEach(function(p) { bucketTotals[p.bucket] = (bucketTotals[p.bucket] || 0) + p.cost; });
var maxBucketCost = Math.max.apply(null, Object.keys(bucketTotals).map(function(k) { return bucketTotals[k]; }));
if (maxBucketCost === 0) maxBucketCost = 1;
buckets.forEach(function(b, i) {
var pct = (bucketTotals[b] / maxBucketCost) * 100;
var shortDate = b.slice(5);
var colorIdx = i % modelColors.length;
tsChart += '<div style="flex:1;display:flex;flex-direction:column;align-items:center;min-width:0">'
+ '<div style="width:100%;background:' + modelColors[colorIdx] + ';border-radius:2px 2px 0 0;height:' + Math.max(pct, 2) + '%;opacity:0.85;transition:height 0.3s" title="' + b + ': $' + bucketTotals[b].toFixed(4) + '"></div>'
+ '<div style="font-size:0.5625rem;color:var(--muted);margin-top:0.25rem;transform:rotate(-45deg);white-space:nowrap">' + shortDate + '</div>'
+ '</div>';
});
tsChart += '</div></div>';
}
// Model comparison table (sortable)
var table = '';
if (modelNames.length > 0) {
table = '<div class="card" style="margin-bottom:1.25rem"><div style="font-weight:600;margin-bottom:0.75rem">Model Comparison</div>'
+ '<div class="table-wrap"><table><thead><tr>'
+ '<th>Model</th><th>Turns</th><th>Output Density</th><th>Cache Hit %</th>'
+ '<th>Cost/Turn</th><th>Total Cost</th><th>Cache Savings</th><th>Cost Trend</th>'
+ '</tr></thead><tbody>';
modelNames.forEach(function(name) {
var m = models[name];
var c = m.cost || {};
var t = m.trend || {};
table += '<tr>'
+ '<td class="card-mono">' + esc(name) + '</td>'
+ '<td>' + m.total_turns.toLocaleString() + '</td>'
+ '<td>' + m.avg_output_density.toFixed(3) + '</td>'
+ '<td>' + (m.cache_hit_rate * 100).toFixed(1) + '%</td>'
+ '<td>$' + c.effective_per_turn.toFixed(4) + '</td>'
+ '<td>$' + c.total.toFixed(4) + '</td>'
+ '<td style="color:var(--success)">$' + c.cache_savings.toFixed(4) + '</td>'
+ '<td>' + trendArrow(c.cumulative_trend) + ' ' + esc(c.cumulative_trend) + '</td>'
+ '</tr>';
});
table += '</tbody></table></div></div>';
}
var assignment = report.subagent_assignment || {};
var assignmentSection = '';
if (assignment.overall && assignment.by_subagent) {
var overall = assignment.overall || {};
var bySubagent = assignment.by_subagent || {};
var assignments = assignment.assignments || {};
var names = Object.keys(bySubagent).sort(function(a, b) {
return (bySubagent[b].total || 0) - (bySubagent[a].total || 0);
});
assignmentSection = '<div class="card" style="margin-bottom:1.25rem"><div style="font-weight:600;margin-bottom:0.75rem">Subagent Assignment Efficacy</div>';
assignmentSection += '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:0.75rem;margin-bottom:0.75rem;font-size:0.8125rem">';
assignmentSection += '<div><span style="color:var(--muted)">Delegations</span><div style="font-weight:600">' + (overall.total || 0) + '</div></div>';
assignmentSection += '<div><span style="color:var(--muted)">Success Rate</span><div style="font-weight:600">' + (((overall.success_rate || 0) * 100).toFixed(1)) + '%</div></div>';
assignmentSection += '<div><span style="color:var(--muted)">Timeout-like</span><div style="font-weight:600;color:var(--warning)">' + (overall.timeout_like || 0) + '</div></div>';
assignmentSection += '<div><span style="color:var(--muted)">P95 Duration</span><div style="font-weight:600">' + (overall.p95_duration_ms || 0).toFixed(0) + ' ms</div></div>';
assignmentSection += '</div>';
assignmentSection += '<div class="table-wrap"><table><thead><tr><th>Subagent</th><th>Assigned Model</th><th>Fallbacks</th><th>Success %</th><th>Timeouts</th><th>Avg ms</th><th>P95 ms</th></tr></thead><tbody>';
names.forEach(function(name) {
var stat = bySubagent[name] || {};
var cfg = assignments[name] || {};
var fallbackList = cfg.fallback_models || [];
assignmentSection += '<tr>'
+ '<td>' + esc(name) + '</td>'
+ '<td class="card-mono">' + esc(cfg.configured_model || '—') + '</td>'
+ '<td>' + esc((fallbackList && fallbackList.length) ? fallbackList.join(', ') : '—') + '</td>'
+ '<td>' + (((stat.success_rate || 0) * 100).toFixed(1)) + '%</td>'
+ '<td>' + (stat.timeout_like || 0) + '</td>'
+ '<td>' + (stat.avg_duration_ms || 0).toFixed(0) + '</td>'
+ '<td>' + (stat.p95_duration_ms || 0).toFixed(0) + '</td>'
+ '</tr>';
});
assignmentSection += '</tbody></table></div></div>';
}
// Inline optimization tips with actionable buttons
var tips = '<div class="card"><div style="font-weight:600;margin-bottom:0.75rem">\u26a1 Quick Optimizations</div><div style="font-size:0.8125rem;color:var(--muted)">';
var tipList = [];
modelNames.forEach(function(name) {
var m = models[name];
if (m.cache_hit_rate < 0.3 && m.total_turns > 5) {
tipList.push(
'<div style="padding:0.375rem 0;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;gap:0.75rem;align-items:center;flex-wrap:wrap">'
+ '<div><span style="color:var(--warning)">\u25cf</span> <strong>' + esc(name) + '</strong> has a low cache hit rate (' + (m.cache_hit_rate * 100).toFixed(0) + '%). Consider enabling semantic caching.</div>'
+ '<button class="btn secondary" data-action="enable-semantic-cache" data-model="' + esc(name) + '" style="font-size:0.6875rem;padding:0.25rem 0.625rem;white-space:nowrap">Enable Caching</button>'
+ '</div>'
);
}
if (m.cost && m.cost.effective_per_turn > 0.05) {
tipList.push('<div style="padding:0.375rem 0;border-bottom:1px solid var(--border)"><span style="color:var(--error)">\u25cf</span> <strong>' + esc(name) + '</strong> costs $' + m.cost.effective_per_turn.toFixed(4) + '/turn. Consider a lighter model for simple tasks.</div>');
}
if (m.avg_output_density < 0.1 && m.total_turns > 5) {
tipList.push('<div style="padding:0.375rem 0;border-bottom:1px solid var(--border)"><span style="color:var(--warning)">\u25cf</span> <strong>' + esc(name) + '</strong> has low output density (' + m.avg_output_density.toFixed(3) + '). Large inputs produce little output.</div>');
}
});
if (tipList.length === 0) {
tipList.push('<div style="padding:0.375rem 0;color:var(--success)">\u2714 All models operating efficiently. No optimizations needed.</div>');
}
tips += tipList.join('') + '</div></div>';
// Store tips for use in Recommendations tab
self._quickTipsHtml = tips;
return tabBar + profileCard + budgetCard + decisionGraph + periodBar + banner + cards + tsChart + table + assignmentSection;
});
};
App._recPeriod = '30d';
App.renderRecommendations = function() {
var self = this;
var period = this._recPeriod || '30d';
return api('/api/recommendations?period=' + encodeURIComponent(period)).then(function(data) {
var recs = data.recommendations || [];
var profile = data.profile || {};
var count = data.count || 0;
var periods = ['7d', '30d', 'all'];
var periodBar = '<div style="display:flex;gap:0.5rem;margin-bottom:1.25rem;flex-wrap:wrap;align-items:center">';
periodBar += '<span style="font-size:0.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-right:0.5rem">Period</span>';
periods.forEach(function(p) {
var cls = p === period ? 'background:var(--accent);color:var(--bg)' : 'background:var(--surface);color:var(--muted);border:1px solid var(--border)';
periodBar += '<button data-rec-period="' + p + '" style="padding:0.375rem 0.875rem;border-radius:4px;font-family:var(--font);font-size:0.8125rem;cursor:pointer;border:none;' + cls + '">' + p.toUpperCase() + '</button>';
});
periodBar += '<button id="btn-deep-analysis" style="margin-left:auto;padding:0.375rem 1rem;border-radius:4px;font-family:var(--font);font-size:0.8125rem;cursor:pointer;border:1px solid var(--accent);background:var(--accent-dim);color:var(--accent)">Generate Deep Analysis</button>';
periodBar += '</div>';
var banner = '<div class="card" style="margin-bottom:1rem;background:linear-gradient(135deg,var(--surface) 0%,rgba(193,128,255,0.08) 100%)">'
+ '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:1rem">'
+ '<div><div style="font-size:0.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.25rem">Recommendations</div><div style="font-size:1.5rem;font-weight:700;color:var(--accent)">' + count + '</div></div>'
+ '<div><div style="font-size:0.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.25rem">Total Turns</div><div style="font-size:1.5rem;font-weight:700">' + (profile.total_turns || 0).toLocaleString() + '</div></div>'
+ '<div><div style="font-size:0.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.25rem">Total Cost</div><div style="font-size:1.5rem;font-weight:700;color:var(--accent)">$' + (profile.total_cost || 0).toFixed(4) + '</div></div>'
+ '<div><div style="font-size:0.7rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:0.25rem">Cache Hit Rate</div><div style="font-size:1.5rem;font-weight:700">' + ((profile.cache_hit_rate || 0) * 100).toFixed(1) + '%</div></div>'
+ '</div></div>';
var priorityColor = function(p) {
if (p === 'High') return 'var(--error)';
if (p === 'Medium') return 'var(--warning)';
return '#c180ff';
};
var priorityBg = function(p) {
if (p === 'High') return 'rgba(239,68,68,0.1)';
if (p === 'Medium') return 'rgba(234,179,8,0.1)';
return 'rgba(193,128,255,0.1)';
};
var categoryIcon = function(cat) {
var icons = {
QueryCrafting: '\u270e',
ModelSelection: '\u2699',
SessionManagement: '\u{1f4ac}',
MemoryLeverage: '\u{1f9e0}',
CostOptimization: '\u{1f4b0}',
ToolUsage: '\u{1f527}',
Configuration: '\u2699'
};
return icons[cat] || '\u2139';
};
var cards = '';
if (recs.length === 0) {
cards = '<div class="card" style="text-align:center;padding:2rem;color:var(--muted)">'
+ '<div style="font-size:1.5rem;margin-bottom:0.5rem">No recommendations</div>'
+ '<div style="font-size:0.875rem">Not enough data to generate recommendations yet. Run more inference requests.</div></div>';
} else {
recs.forEach(function(rec, idx) {
var color = priorityColor(rec.priority);
var bg = priorityBg(rec.priority);
var icon = categoryIcon(rec.category);
var evidenceId = 'rec-evidence-' + idx;
cards += '<div class="card" style="border-left:3px solid ' + color + ';margin-bottom:0.75rem">'
+ '<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:0.5rem">'
+ '<div style="display:flex;align-items:center;gap:0.5rem">'
+ '<span style="font-size:1.125rem">' + icon + '</span>'
+ '<div style="font-weight:600;font-size:0.9375rem">' + esc(rec.title) + '</div>'
+ '</div>'
+ '<span style="background:' + bg + ';color:' + color + ';padding:0.125rem 0.625rem;border-radius:3px;font-size:0.6875rem;font-weight:600;text-transform:uppercase;flex-shrink:0">' + esc(rec.priority) + '</span>'
+ '</div>';
cards += '<div style="font-size:0.8125rem;color:var(--muted);margin-bottom:0.5rem">' + esc(rec.explanation) + '</div>';
cards += '<div style="font-size:0.8125rem;padding:0.5rem 0.75rem;background:rgba(193,128,255,0.06);border-radius:4px;margin-bottom:0.5rem"><strong style="color:var(--accent)">Action:</strong> ' + esc(rec.action) + '</div>';
if (rec.estimated_impact) {
var imp = rec.estimated_impact;
var impactParts = [];
if (imp.monthly_savings) impactParts.push('<span style="color:var(--success)">Est. savings: $' + imp.monthly_savings.toFixed(4) + '</span>');
if (imp.quality_change) impactParts.push('<span style="color:var(--accent)">' + esc(imp.quality_change) + '</span>');
if (impactParts.length > 0) {
cards += '<div style="font-size:0.75rem;display:flex;gap:1rem;margin-bottom:0.5rem">' + impactParts.join('') + '</div>';
}
}
if (rec.evidence && rec.evidence.length > 0) {
cards += '<div style="margin-top:0.25rem">'
+ '<button data-rec-evidence-toggle="' + evidenceId + '" aria-expanded="false" style="background:none;border:none;color:var(--muted);font-family:var(--font);font-size:0.75rem;cursor:pointer;padding:0">\u25b6 Evidence (' + rec.evidence.length + ')</button>'
+ '<div id="' + evidenceId + '" style="display:none;margin-top:0.375rem">';
rec.evidence.forEach(function(ev) {
cards += '<div style="font-size:0.75rem;padding:0.25rem 0;border-bottom:1px solid var(--border)">'
+ '<span style="color:var(--accent)">' + esc(ev.metric) + '</span>: '
+ '<span style="font-weight:600">' + esc(ev.value) + '</span>'
+ ' <span style="color:var(--muted)">(' + esc(ev.context) + ')</span></div>';
});
cards += '</div></div>';
}
cards += '</div>';
});
}
var deepAnalysisContainer = '<div id="deep-analysis-result" style="display:none;margin-top:1rem"></div>';
setTimeout(function() {
document.querySelectorAll('[data-rec-period]').forEach(function(btn) {
btn.addEventListener('click', function() {
self._recPeriod = btn.getAttribute('data-rec-period');
self._effTab = 'recommendations';
self.navigate('efficiency');
});
});
var deepBtn = document.getElementById('btn-deep-analysis');
if (deepBtn) {
deepBtn.addEventListener('click', function() {
deepBtn.disabled = true;
deepBtn.textContent = 'Generating...';
api('/api/recommendations/generate?period=' + encodeURIComponent(self._recPeriod || '30d'), { method: 'POST' }).then(function(res) {
var container = document.getElementById('deep-analysis-result');
if (container) {
container.style.display = 'block';
var actions = (res.actions || []).map(function(a) { return '<li style="margin:0.25rem 0">' + esc(a) + '</li>'; }).join('');
var savings = (res.summary && res.summary.estimated_monthly_savings != null) ? '$' + Number(res.summary.estimated_monthly_savings).toFixed(4) : '—';
var html = '<div class="card"><div style="font-weight:600;margin-bottom:0.75rem">Deep Analysis</div>'
+ '<div style="font-size:0.8125rem;color:var(--muted);margin-bottom:0.5rem">' + esc(res.message || 'Analysis complete') + '</div>'
+ '<div style="font-size:0.75rem;color:var(--text);margin-bottom:0.75rem"><strong>Estimated monthly savings:</strong> ' + esc(savings) + '</div>'
+ (actions ? '<div style="font-size:0.75rem;margin-bottom:0.375rem;color:var(--muted)">Prioritized actions</div><ol style="margin:0;padding-left:1rem;font-size:0.75rem">' + actions + '</ol>' : '<div style="font-size:0.75rem;color:var(--muted)">No prioritized actions returned.</div>')
+ '</div>';
setHtml(container, html);
}
deepBtn.disabled = false;
deepBtn.textContent = 'Generate Deep Analysis';
}).catch(function() {
deepBtn.disabled = false;
deepBtn.textContent = 'Generate Deep Analysis';
});
});
}
}, 0);
return periodBar + banner + cards + deepAnalysisContainer;
});
};
App._TOKEN_ICONS = { ETH: '\u039e', MATIC: '\u25c6', USDC: '\u0024', USDT: '\u0024', DAI: '\u25c7', WETH: '\u039e', WBTC: '\u20bf', cbBTC: '\u20bf' };
App._renderRevenueSwapSummaryCard = function(revenueSwap) {
var cfg = revenueSwap || {};
var enabled = cfg.enabled !== false;
var target = cfg.target_symbol || 'PUSD';
var chain = cfg.default_chain || 'ETH';
var chains = Array.isArray(cfg.chains) ? cfg.chains : [];
var chainRows = chains.map(function(entry) {
var swapContract = entry.swap_contract_address
? '<div style="font-size:0.625rem;color:var(--muted);margin-top:0.2rem">Swap contract: <span class="card-mono">' + esc(entry.swap_contract_address) + '</span></div>'
: '<div style="font-size:0.625rem;color:var(--muted);margin-top:0.2rem">Swap contract: <span class="card-mono">custom/no override</span></div>';
return '<div class="settings-nested" style="margin-top:0.6rem">'
+ '<div class="settings-row"><div class="settings-label">' + esc(entry.chain || 'UNSPECIFIED') + '</div><div class="card-mono" style="font-size:0.7rem;word-break:break-all">' + esc(entry.target_contract_address || '\u2014') + '</div></div>'
+ swapContract
+ '</div>';
}).join('');
if (!chainRows) chainRows = '<div style="font-size:0.75rem;color:var(--muted);padding-top:0.35rem">No chain targets configured.</div>';
return '<div class="card" style="margin-bottom:1rem"><div class="card-title">Revenue Swap Policy</div>'
+ '<div class="settings-row"><div class="settings-label">Auto-swap</div><div style="font-family:var(--font-mono)">' + (enabled ? 'enabled' : 'disabled') + '</div></div>'
+ '<div class="settings-row"><div class="settings-label">Default target</div><div style="font-family:var(--font-mono)">' + esc(target) + '</div></div>'
+ '<div class="settings-row"><div class="settings-label">Default chain</div><div style="font-family:var(--font-mono)">' + esc(chain) + '</div></div>'
+ chainRows
+ '</div>';
};
App._renderRevenueStrategySummaryCard = function(rows) {
var items = Array.isArray(rows) ? rows : [];
var body = items.length
? items.map(function(row) {
return '<div class="settings-row">'
+ '<div><div style="font-weight:600;font-size:0.8125rem">' + esc(row.strategy || 'unknown') + '</div><div style="font-size:0.625rem;color:var(--muted)">' + esc(String(row.settled_jobs || 0)) + ' settled / ' + esc(String(row.total_jobs || 0)) + ' total</div></div>'
+ '<div style="text-align:right;font-family:var(--font-mono)"><div>$' + Number(row.net_profit_usdc || 0).toFixed(2) + '</div><div style="font-size:0.625rem;color:var(--muted)">priority ' + Number(row.avg_priority_score || 0).toFixed(1) + '</div></div>'
+ '</div>';
}).join('')
: '<div style="font-size:0.75rem;color:var(--muted)">No settled strategy data yet.</div>';
return '<div class="card" style="margin-bottom:1rem"><div class="card-title">Revenue Strategy Summary</div>' + body + '</div>';
};
App._renderRevenueFeedbackSummaryCard = function(rows) {
var items = Array.isArray(rows) ? rows : [];
var body = items.length
? items.map(function(row) {
return '<div class="settings-row">'
+ '<div><div style="font-weight:600;font-size:0.8125rem">' + esc(row.strategy || 'unknown') + '</div><div style="font-size:0.625rem;color:var(--muted)">' + esc(String(row.feedback_count || 0)) + ' feedback signals</div></div>'
+ '<div style="text-align:right;font-family:var(--font-mono)"><div>' + Number(row.avg_grade || 0).toFixed(2) + ' / 5.00</div><div style="font-size:0.625rem;color:var(--muted)">' + esc((row.latest_feedback_at || '').replace('T', ' ').slice(0, 19) || '\u2014') + '</div></div>'
+ '</div>';
}).join('')
: '<div style="font-size:0.75rem;color:var(--muted)">No feedback recorded yet.</div>';
return '<div class="card" style="margin-bottom:1rem"><div class="card-title">Revenue Feedback Summary</div>' + body + '</div>';
};
App._renderSeedExerciseReadinessCard = function(readiness) {
var r = readiness || {};
var ready = !!r.meets_seed_target;
var reserve = !!r.minimum_reserve_configured;
var swapEnabled = !!r.swap_enabled;
var targetOk = !!r.default_chain_has_target_contract;
var swapOk = !!r.default_chain_has_swap_contract;
var target = Number(r.seed_target_usdc || 50);
var stable = Number(r.stable_balance_usdc || 0);
var defaultChain = r.default_chain || 'ETH';
var checks = [
['Stable balance', stable.toFixed(2) + ' / ' + target.toFixed(2) + ' USDC', ready],
['Minimum reserve', reserve ? 'configured' : 'missing', reserve],
['Auto-swap', swapEnabled ? 'enabled' : 'disabled', swapEnabled],
['Target contract', targetOk ? 'configured' : 'missing', targetOk],
['Swap contract', swapOk ? 'configured' : 'missing', swapOk]
];
var rows = checks.map(function(check) {
return '<div class="settings-row">'
+ '<div class="settings-label">' + esc(check[0]) + '</div>'
+ '<div style="display:flex;align-items:center;gap:0.5rem"><span class="badge ' + (check[2] ? 'success' : 'warn') + '">' + esc(check[1]) + '</span></div>'
+ '</div>';
}).join('');
return '<div class="card" style="margin-bottom:1rem"><div class="card-title">$50 Seed Readiness</div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.75rem">Default chain: <span class="card-mono">' + esc(defaultChain) + '</span></div>'
+ rows
+ '</div>';
};
App._renderSeedExerciseProgressCard = function(progress) {
var p = progress || {};
var phases = [
['Phase 1: seeded + visible', !!p.phase_1_seeded_and_visible],
['Phase 1: target met', !!p.phase_1_meets_target],
['Phase 2: revenue cycle complete', !!p.phase_2_revenue_cycle_complete],
['Phase 3: swap submitted', !!p.phase_3_swap_submitted],
['Phase 3: swap reconciled', !!p.phase_3_swap_reconciled],
['Phase 3: tax submitted', !!p.phase_3_tax_submitted],
['Phase 3: tax reconciled', !!p.phase_3_tax_reconciled],
['Phase 4: mechanic clear', !!p.phase_4_mechanic_clear]
];
var rows = phases.map(function(phase) {
return '<div class="settings-row">'
+ '<div class="settings-label">' + esc(phase[0]) + '</div>'
+ '<div><span class="badge ' + (phase[1] ? 'success' : 'warn') + '">' + (phase[1] ? 'done' : 'pending') + '</span></div>'
+ '</div>';
}).join('');
return '<div class="card" style="margin-bottom:1rem"><div class="card-title">Seed Exercise Progress</div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.75rem">Next action: ' + esc(p.next_action || 'no action available') + '</div>'
+ rows
+ '</div>';
};
App._renderSeedExercisePlanCard = function(plan) {
var p = plan || {};
var phases = Array.isArray(p.phases) ? p.phases : [];
var phaseRows = phases.length
? phases.map(function(phase) {
return '<div class="settings-nested" style="margin-top:0.6rem">'
+ '<div class="settings-row"><div class="settings-label">' + esc(phase.label || phase.id || 'phase') + '</div><div><span class="badge">' + esc(String(Number(phase.max_spend_usdc || 0).toFixed(2))) + ' USDC cap</span></div></div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-top:0.35rem">' + esc(phase.goal || '') + '</div>'
+ '<div style="font-size:0.6875rem;color:var(--muted);margin-top:0.35rem">Success signal: ' + esc(phase.success_signal || '') + '</div>'
+ '</div>';
}).join('')
: '<div style="font-size:0.75rem;color:var(--muted)">No seed exercise plan is available.</div>';
var aborts = Array.isArray(p.abort_conditions) ? p.abort_conditions : [];
var guidance = Array.isArray(p.operator_guidance) ? p.operator_guidance : [];
return '<div class="card" style="margin-bottom:1rem"><div class="card-title">$50 Seed Exercise Plan</div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.75rem">Constrained live-funds validation plan for treasury, settlement, routing, and repair.</div>'
+ phaseRows
+ '<div style="margin-top:0.9rem;font-size:0.75rem;color:var(--muted)">Abort conditions</div>'
+ (aborts.length ? '<ul style="margin:0.35rem 0 0 1rem;padding:0;color:var(--text)">' + aborts.map(function(item) { return '<li style="margin:0.3rem 0">' + esc(item) + '</li>'; }).join('') + '</ul>' : '<div style="font-size:0.75rem;color:var(--muted)">No abort conditions defined.</div>')
+ '<div style="margin-top:0.9rem;font-size:0.75rem;color:var(--muted)">Operator guidance</div>'
+ (guidance.length ? '<ul style="margin:0.35rem 0 0 1rem;padding:0;color:var(--text)">' + guidance.map(function(item) { return '<li style="margin:0.3rem 0">' + esc(item) + '</li>'; }).join('') + '</ul>' : '<div style="font-size:0.75rem;color:var(--muted)">No operator guidance defined.</div>')
+ '</div>';
};
App._renderRevenueSwapTasksCard = function(rows) {
var items = Array.isArray(rows) ? rows : [];
var body = items.length
? '<div class="table-wrap"><table><thead><tr><th>Opportunity</th><th>Status</th><th>Target</th><th>Tx</th><th>Updated</th><th>Actions</th></tr></thead><tbody>'
+ items.map(function(row) {
var source = row.source || {};
var target = (source.target_asset || 'unknown') + ' on ' + (source.target_chain || 'unknown');
var txHash = source.swap_tx_hash
? '<span class="card-mono" style="font-size:0.625rem">' + esc(String(source.swap_tx_hash).slice(0, 18)) + '\u2026</span>'
: '<span style="color:var(--muted)">\u2014</span>';
var updated = row.updated_at ? String(row.updated_at).replace('T', ' ').slice(0, 19) : '\u2014';
var actions = [];
if (row.status === 'pending') {
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="swap" data-revenue-task-action="start" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Start</button>');
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="swap" data-revenue-task-action="submit" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Submit</button>');
} else if (row.status === 'in_progress') {
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="swap" data-revenue-task-action="reconcile" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Reconcile</button>');
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="swap" data-revenue-task-action="confirm" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Confirm</button>');
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="swap" data-revenue-task-action="fail" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Fail</button>');
}
return '<tr>'
+ '<td><div style="font-weight:600">' + esc(row.opportunity_id || row.id || 'unknown') + '</div><div style="font-size:0.625rem;color:var(--muted)">' + esc(row.title || '') + '</div></td>'
+ '<td>' + esc(row.status || 'unknown') + '</td>'
+ '<td>' + esc(target) + '</td>'
+ '<td>' + txHash + '</td>'
+ '<td style="color:var(--muted)">' + esc(updated) + '</td>'
+ '<td><div style="display:flex;flex-wrap:wrap;gap:0.25rem">' + (actions.join('') || '<span style="color:var(--muted)">—</span>') + '</div></td>'
+ '</tr>';
}).join('')
+ '</tbody></table></div>'
: '<div style="font-size:0.75rem;color:var(--muted)">No revenue swap tasks yet.</div>';
return '<div class="card" style="margin-bottom:1rem"><div class="card-title">Revenue Swap Tasks</div>' + body + '</div>';
};
App._renderRevenueTaxTasksCard = function(rows) {
var items = Array.isArray(rows) ? rows : [];
var body = items.length
? '<div class="table-wrap"><table><thead><tr><th>Opportunity</th><th>Status</th><th>Destination</th><th>Tx</th><th>Updated</th><th>Actions</th></tr></thead><tbody>'
+ items.map(function(row) {
var source = row.source || {};
var dest = (source.destination_wallet || 'unknown') + ' on ' + (source.target_chain || 'unknown');
var txHash = source.tax_tx_hash
? '<span class="card-mono" style="font-size:0.625rem">' + esc(String(source.tax_tx_hash).slice(0, 18)) + '…</span>'
: '<span style="color:var(--muted)">—</span>';
var updated = row.updated_at ? String(row.updated_at).replace('T', ' ').slice(0, 19) : '—';
var actions = [];
if (row.status === 'pending') {
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="tax" data-revenue-task-action="start" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Start</button>');
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="tax" data-revenue-task-action="submit" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Submit</button>');
} else if (row.status === 'in_progress') {
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="tax" data-revenue-task-action="reconcile" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Reconcile</button>');
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="tax" data-revenue-task-action="confirm" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Confirm</button>');
actions.push('<button class="btn secondary" style="font-size:0.625rem;padding:0.18rem 0.45rem" data-revenue-task-kind="tax" data-revenue-task-action="fail" data-opportunity-id="' + esc(row.opportunity_id || '') + '">Fail</button>');
}
return '<tr>'
+ '<td><div style="font-weight:600">' + esc(row.opportunity_id || row.id || 'unknown') + '</div><div style="font-size:0.625rem;color:var(--muted)">' + esc(row.title || '') + '</div></td>'
+ '<td>' + esc(row.status || 'unknown') + '</td>'
+ '<td>' + esc(dest) + '</td>'
+ '<td>' + txHash + '</td>'
+ '<td style="color:var(--muted)">' + esc(updated) + '</td>'
+ '<td><div style="display:flex;flex-wrap:wrap;gap:0.25rem">' + (actions.join('') || '<span style="color:var(--muted)">—</span>') + '</div></td>'
+ '</tr>';
}).join('')
+ '</tbody></table></div>'
: '<div style="font-size:0.75rem;color:var(--muted)">No revenue tax tasks yet.</div>';
return '<div class="card" style="margin-bottom:1rem"><div class="card-title">Revenue Tax Tasks</div>' + body + '</div>';
};
App._renderRevenueSwapSettings = function(revenueSwap) {
var cfg = revenueSwap || {};
var chains = Array.isArray(cfg.chains) ? cfg.chains : [];
var rows = chains.map(function(entry, idx) {
return '<div class="settings-nested" style="margin-top:0.6rem">'
+ '<div class="settings-row"><div class="settings-label">Chain</div><input class="settings-input" type="text" data-revenue-swap-chain-field="chain" data-revenue-swap-chain-index="' + idx + '" value="' + esc(entry.chain || '') + '" placeholder="ETH"></div>'
+ '<div class="settings-row"><div class="settings-label">Target contract</div><input class="settings-input" type="text" data-revenue-swap-chain-field="target_contract_address" data-revenue-swap-chain-index="' + idx + '" value="' + esc(entry.target_contract_address || '') + '" placeholder="0x... or program id"></div>'
+ '<div class="settings-row"><div class="settings-label">Swap contract</div><input class="settings-input" type="text" data-revenue-swap-chain-field="swap_contract_address" data-revenue-swap-chain-index="' + idx + '" value="' + esc(entry.swap_contract_address || '') + '" placeholder="optional"></div>'
+ '<div style="display:flex;justify-content:flex-end"><button class="btn secondary" data-revenue-swap-remove-chain="' + idx + '">Remove chain</button></div>'
+ '</div>';
}).join('');
if (!rows) rows = '<div style="font-size:0.75rem;color:var(--muted)">No chain targets configured yet. Add one below.</div>';
return '<div class="settings-section"><div class="settings-section-title">Revenue Swap</div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.75rem">Default post-settlement conversion policy. Users can disable auto-swap, change the target asset, or configure any chain so long as the contract addresses are supplied.</div>'
+ '<div class="settings-row"><div class="settings-label">Auto-swap by default</div><div class="settings-toggle-wrap"><label class="toggle"><input type="checkbox" data-settings-path="treasury.revenue_swap.enabled"' + (cfg.enabled !== false ? ' checked' : '') + '><span class="toggle-track"></span></label><span class="settings-toggle-label">' + (cfg.enabled !== false ? 'On' : 'Off') + '</span></div></div>'
+ '<div class="settings-row"><div class="settings-label">Target symbol</div><input class="settings-input" type="text" data-settings-path="treasury.revenue_swap.target_symbol" value="' + esc(cfg.target_symbol || 'PUSD') + '" placeholder="PUSD"></div>'
+ '<div class="settings-row"><div class="settings-label">Default chain</div><input class="settings-input" type="text" data-settings-path="treasury.revenue_swap.default_chain" value="' + esc(cfg.default_chain || 'ETH') + '" placeholder="ETH"></div>'
+ '<div class="settings-row"><div class="settings-label">Configured chains</div><div style="width:100%">' + rows + '</div></div>'
+ '<div class="model-order-add" style="margin-top:0.5rem"><input id="revenue-swap-chain-add" class="settings-input" type="text" placeholder="Add chain (e.g. ETH, SOLANA, BSC, ARBITRUM)"><button class="btn secondary" id="revenue-swap-chain-add-btn">Add chain</button></div>'
+ '</div>';
};