eq performance
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"job": "none",
|
"job": "none",
|
||||||
"emails": [
|
"emails": [],
|
||||||
"zipfriis@gmail.com"
|
|
||||||
],
|
|
||||||
"newletter-sent": 0
|
"newletter-sent": 0
|
||||||
}
|
}
|
||||||
@@ -19,8 +19,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Job string `json:"job"`
|
Job string `json:"job"`
|
||||||
Emails []string `json:"emails"`
|
Emails []string `json:"emails"`
|
||||||
|
NewsSent int `json:"newletter-sent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() (*Config, error) {
|
func loadConfig() (*Config, error) {
|
||||||
@@ -187,6 +188,15 @@ func main() {
|
|||||||
fmt.Fprintf(w, "%s", strconv.Itoa(len(conf.Emails)))
|
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
|
// strip /admin/api before handing off
|
||||||
adminMux.Handle("/api/", http.StripPrefix("/api", apiMux))
|
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>
|
<a class="outlink" href="/static/pdf/novo.pdf" target="_blank">Read file</a>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<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>
|
||||||
<section id="newsletter">
|
<section id="newsletter">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -436,7 +447,7 @@
|
|||||||
<div class="footer-copy">© 2026 — all rights reserved</div>
|
<div class="footer-copy">© 2026 — all rights reserved</div>
|
||||||
</footer>
|
</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 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>
|
<span class="spinner">Loading login page...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user