eq performance
This commit is contained in:
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"job": "none",
|
||||
"emails": [
|
||||
"zipfriis@gmail.com"
|
||||
],
|
||||
"emails": [],
|
||||
"newletter-sent": 0
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
type Config struct {
|
||||
Job string `json:"job"`
|
||||
Emails []string `json:"emails"`
|
||||
NewsSent int `json:"newletter-sent"`
|
||||
}
|
||||
|
||||
func loadConfig() (*Config, error) {
|
||||
@@ -187,6 +188,15 @@ func main() {
|
||||
fmt.Fprintf(w, "%s", strconv.Itoa(len(conf.Emails)))
|
||||
}))
|
||||
|
||||
apiMux.HandleFunc("GET /stats/sent", requireAuth(func(w http.ResponseWriter, r *http.Request) {
|
||||
conf, err := loadConfig()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
||||
}
|
||||
fmt.Fprintf(w, "%s", strconv.Itoa(conf.NewsSent))
|
||||
}))
|
||||
|
||||
// strip /admin/api before handing off
|
||||
adminMux.Handle("/api/", http.StripPrefix("/api", apiMux))
|
||||
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@300;400;500&display=swap');
|
||||
.eq-wrap { font-family: 'IBM Plex Sans', monospace; padding: 2rem 0 1rem; }
|
||||
.eq-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.25rem; }
|
||||
.eq-title { font-family: 'IBM Plex Mono', monospace; font-size: 11px; font-weight: 500; letter-spacing: 0.12em; color: var(--color-text-secondary); text-transform: uppercase; }
|
||||
.eq-ytd { font-family: 'IBM Plex Mono', monospace; font-size: 22px; font-weight: 500; color: var(--color-text-primary); margin: 0.1rem 0 1.5rem; }
|
||||
.eq-ytd span { font-size: 13px; font-weight: 400; color: #1D9E75; margin-left: 8px; }
|
||||
.eq-divider { border: none; border-top: 0.5px solid var(--color-border-tertiary); margin-bottom: 1.5rem; }
|
||||
.eq-legend { display: flex; gap: 20px; margin-bottom: 0.75rem; font-size: 12px; font-family: 'IBM Plex Mono', monospace; color: var(--color-text-secondary); align-items: center; }
|
||||
.eq-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-top: 1.5rem; }
|
||||
.eq-stat { background: var(--color-background-secondary); border-radius: var(--border-radius-md); padding: 0.75rem 1rem; }
|
||||
.eq-stat-label { font-size: 11px; font-family: 'IBM Plex Mono', monospace; color: var(--color-text-secondary); letter-spacing: 0.05em; margin-bottom: 4px; }
|
||||
.eq-stat-val { font-size: 16px; font-weight: 500; font-family: 'IBM Plex Mono', monospace; }
|
||||
.eq-stat-val.pos { color: #1D9E75; }
|
||||
.eq-stat-val.neg { color: #E24B4A; }
|
||||
.eq-note { font-size: 11px; color: var(--color-text-secondary); margin-top: 1rem; font-family: 'IBM Plex Mono', monospace; }
|
||||
</style>
|
||||
|
||||
<div class="eq-wrap">
|
||||
<p class="eq-title">Equity · Portfolio Performance</p>
|
||||
<div class="eq-header">
|
||||
<div class="eq-ytd">+9.58% <span>YTD</span></div>
|
||||
<span style="font-size:11px; font-family:'IBM Plex Mono',monospace; color:var(--color-text-secondary);">2026</span>
|
||||
</div>
|
||||
<hr class="eq-divider">
|
||||
|
||||
<div class="eq-legend">
|
||||
<span style="display:flex;align-items:center;gap:6px;">
|
||||
<span style="width:16px;height:2px;background:#1D9E75;border-radius:1px;display:inline-block;"></span>cumulative
|
||||
</span>
|
||||
<span style="display:flex;align-items:center;gap:6px;">
|
||||
<span style="width:16px;height:10px;background:rgba(29,158,117,0.25);border:1px solid #1D9E75;border-radius:2px;display:inline-block;"></span>monthly
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="position:relative; width:100%; height:220px;">
|
||||
<canvas id="eqChart" role="img" aria-label="Line chart of cumulative and monthly equity returns. Cumulative YTD is plus 9.58%.">
|
||||
Monthly: Jan +8.13%, Feb −8.84%, Mar +0.41%, Apr +6.59%, May +3.20%, Now +0.64%. Cumulative: Jan +8.13%, Feb −1.43%, Mar −1.02%, Apr +5.51%, May +8.89%, Now +9.58%.
|
||||
</canvas>
|
||||
</div>
|
||||
|
||||
<div class="eq-stats">
|
||||
<div class="eq-stat">
|
||||
<div class="eq-stat-label">best month</div>
|
||||
<div class="eq-stat-val pos">+8.13%</div>
|
||||
</div>
|
||||
<div class="eq-stat">
|
||||
<div class="eq-stat-label">worst month</div>
|
||||
<div class="eq-stat-val neg">−8.84%</div>
|
||||
</div>
|
||||
<div class="eq-stat">
|
||||
<div class="eq-stat-label">cumulative YTD</div>
|
||||
<div class="eq-stat-val pos">+9.58%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="eq-note">// data through Jun 2026 · monthly returns & cumulative</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const monthly = [8.13, -8.84, 0.41, 6.59, 3.20, 0.64];
|
||||
const cumul = [8.13, -1.43, -1.02, 5.51, 8.89, 9.58];
|
||||
const labels = ['jan','feb','mar','apr','may','now'];
|
||||
|
||||
function initChart() {
|
||||
const isDark = matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const gridColor = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)';
|
||||
const textColor = isDark ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)';
|
||||
const zeroColor = isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.15)';
|
||||
|
||||
new Chart(document.getElementById('eqChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
type: 'line',
|
||||
label: 'cumulative',
|
||||
data: cumul,
|
||||
borderColor: '#1D9E75',
|
||||
borderWidth: 2,
|
||||
pointBackgroundColor: '#1D9E75',
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6,
|
||||
tension: 0.35,
|
||||
fill: { target: 'origin', above: 'rgba(29,158,117,0.08)', below: 'rgba(226,75,74,0.08)' },
|
||||
yAxisID: 'y',
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
type: 'bar',
|
||||
label: 'monthly',
|
||||
data: monthly,
|
||||
backgroundColor: monthly.map(v => v >= 0 ? 'rgba(29,158,117,0.25)' : 'rgba(226,75,74,0.25)'),
|
||||
borderColor: monthly.map(v => v >= 0 ? '#1D9E75' : '#E24B4A'),
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
yAxisID: 'y',
|
||||
order: 2
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: isDark ? '#2C2C2A' : '#fff',
|
||||
borderColor: isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.1)',
|
||||
borderWidth: 1,
|
||||
titleColor: textColor,
|
||||
bodyColor: isDark ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0.8)',
|
||||
titleFont: { family: "'IBM Plex Mono', monospace", size: 11 },
|
||||
bodyFont: { family: "'IBM Plex Mono', monospace", size: 12 },
|
||||
padding: 10,
|
||||
callbacks: {
|
||||
label: ctx => {
|
||||
const sign = ctx.parsed.y >= 0 ? '+' : '';
|
||||
return ` ${ctx.dataset.label}: ${sign}${ctx.parsed.y.toFixed(2)}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: { display: false },
|
||||
border: { display: false },
|
||||
ticks: { color: textColor, font: { family: "'IBM Plex Mono', monospace", size: 11 }, autoSkip: false }
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
color: ctx => ctx.tick.value === 0 ? zeroColor : gridColor,
|
||||
lineWidth: ctx => ctx.tick.value === 0 ? 1 : 0.5
|
||||
},
|
||||
border: { display: false },
|
||||
ticks: {
|
||||
color: textColor,
|
||||
font: { family: "'IBM Plex Mono', monospace", size: 11 },
|
||||
callback: v => (v >= 0 ? '+' : '') + v.toFixed(1) + '%'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof Chart !== 'undefined') {
|
||||
initChart();
|
||||
} else {
|
||||
var s = document.createElement('script');
|
||||
s.src = 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js';
|
||||
s.onload = initChart;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
+12
-1
@@ -249,6 +249,17 @@
|
||||
<a class="outlink" href="/static/pdf/novo.pdf" target="_blank">Read file</a>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div class="section-head">
|
||||
<h2>Equity portfolio performance</h2>
|
||||
</div>
|
||||
<div class="exp-card">
|
||||
<div hx-get="/static/components/eq.html" hx-trigger="load" hx-swap="outerHTML">
|
||||
<span class="spinner">Loading chart...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</section>
|
||||
<section id="newsletter">
|
||||
<div class="page-header">
|
||||
@@ -436,7 +447,7 @@
|
||||
<div class="footer-copy">© 2026 — all rights reserved</div>
|
||||
</footer>
|
||||
<div id="login-overlay" style="min-width: 50%; position: fixed; inset: 0; z-index: 100; display: none; align-items: center; justify-content: center; background: rgba(0,0,0,0.35); backdrop-filter: blur(12px);">
|
||||
<div hx-get="login" hx-trigger="load" hx-swap="outerHTML">
|
||||
<div hx-get="/login" hx-trigger="load" hx-swap="outerHTML">
|
||||
<span class="spinner">Loading login page...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user