Files
Portfolio-Engine/internal/website/main.go
2026-04-29 08:38:41 +02:00

240 lines
6.7 KiB
Go

package website
import (
"Portifolio/internal/model"
"Portifolio/internal/service"
_ "embed"
"fmt"
"net/http"
"strconv"
_ "github.com/mattn/go-sqlite3"
)
//go:embed static/index.html
var indexHTML []byte
func Getsite() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(indexHTML)
}
}
//go:embed static/styles.css
var styleCSS []byte
func Getstylesheet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css; charset=utf-8")
w.Write(styleCSS)
}
}
func HealthFragment(svc *service.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h := svc.CheckHealth()
status := h["status"].(string)
uptime := h["uptime"].(string)
dotClass := "ok"
statusText := "backend ok"
if status != "ok" {
dotClass = "error"
statusText = "backend " + status
w.WriteHeader(http.StatusServiceUnavailable) // ← triggers htmx:responseError
}
dbInfo := h["database"].(map[string]any)
dbStatus := dbInfo["status"].(string)
dbLatency := dbInfo["latency"].(string)
dbText := "db " + dbStatus
if dbLatency != "" {
dbText += " · " + dbLatency
}
fmt.Fprintf(w, `
<span class="status-dot %s"></span>
<span>%s</span>
<span class="status-sep">·</span>
<span style="color:var(--muted)">up %s</span>
<span class="status-sep">·</span>
<span style="color:var(--muted)">%s</span>
<span class="status-spacer"></span>
<button class="refresh-btn"
hx-get="/health/fragment"
hx-target="#statusBar"
hx-swap="innerHTML">↺ refresh</button>
`, dotClass, statusText, uptime, dbText)
}
}
// GET /positions/fragment — returns <tr> rows for positions tbody
func PositionsFragment(svc *service.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
positions, err := svc.GetPositions()
if err != nil {
fmt.Fprintf(w, `<tr class="state-row"><td colspan="5">load failed: %s</td></tr>`, err)
return
}
if len(positions) == 0 {
fmt.Fprint(w, `<tr class="state-row"><td colspan="5">no positions</td></tr>`)
return
}
var total float64
for _, p := range positions {
total += p.CostBasis
}
for _, p := range positions {
weight := 0.0 // ← renamed from w to avoid shadowing http.ResponseWriter
if total > 0 {
weight = p.CostBasis / total * 100
}
fmt.Fprintf(w, `<tr>
<td><span class="ticker">%s</span></td>
<td><span class="currency-badge">%s</span></td>
<td>%s</td>
<td>%s</td>
<td><div class="weight-cell">%.1f%%
<div class="weight-bar-track">
<div class="weight-bar-fill" style="width:%.0f%%"></div>
</div>
</div></td>
</tr>`, p.Symbol, p.CurrencyCode, fmtInt(p.Shares), fmtNum(p.CostBasis), weight, weight)
}
}
}
// GET /positions/fragment — returns <tr> rows for positions tbody
func CompanyFragment(svc *service.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
companies, err := svc.GetAllCompanies()
if err != nil {
fmt.Fprintf(w, `<tr class="state-row"><td colspan="5">load failed: %s</td></tr>`, err)
return
}
if len(companies) == 0 {
fmt.Fprint(w, `<tr class="state-row"><td colspan="5">no positions</td></tr>`)
return
}
for _, c := range companies {
fmt.Fprintf(w, `<tr>
<td><span class="ticker">%s</span></td>
<td>%s</td>
<td>%.2f</td>
<td>%d</td>
<td>%.2f</td>
</tr>`, c.Symbol, c.CurrencyCode, c.Price, c.SharesOutstanding, c.Price*float64(c.SharesOutstanding))
}
}
}
// GET /positions/fragment — returns <tr> rows for positions tbody
func SummaryFragment(svc *service.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
positions, err := svc.GetPositions()
if err != nil {
fmt.Fprintf(w, `<tr class="state-row"><td colspan="5">load failed: %s</td></tr>`, err)
return
}
if len(positions) == 0 {
fmt.Fprint(w, `<tr class="state-row"><td colspan="5">no positions</td></tr>`)
return
}
var total float64
for _, p := range positions {
total += p.CostBasis
}
for _, p := range positions {
weight := 0.0 // ← renamed from w to avoid shadowing http.ResponseWriter
if total > 0 {
weight = p.CostBasis / total * 100
}
fmt.Fprintf(w, `<tr>
<td><span class="ticker">%s</span></td>
<td><span class="currency-badge">%s</span></td>
<td>%s</td>
<td>%s</td>
<td><div class="weight-cell">%.1f%%
<div class="weight-bar-track">
<div class="weight-bar-fill" style="width:%.0f%%"></div>
</div>
</div></td>
</tr>`, p.Symbol, p.CurrencyCode, fmtInt(p.Shares), fmtNum(p.CostBasis), weight, weight)
}
}
}
func fmtNum(v float64) string {
if v == 0 {
return "—"
}
return strconv.FormatFloat(v, 'f', 2, 64)
}
func fmtInt(v int) string {
return strconv.Itoa(v)
}
var productLabels = map[model.TradeProduct]string{
model.StockTrade: "Stock",
model.OptionCallTrade: "Call Option",
model.OptionPutTrade: "Put Option",
model.CurrencyTrade: "Currency",
model.BondTrade: "Bond",
}
func TradeFragment(svc *service.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
trades, err := svc.GetTrades()
if err != nil {
fmt.Fprintf(w, `<tr class="state-row"><td colspan="7">load failed: %s</td></tr>`, err)
return
}
if len(trades) == 0 {
fmt.Fprint(w, `<tr class="state-row"><td colspan="7">no trades</td></tr>`)
return
}
for _, t := range trades {
dir, cls := "SELL", "dir-sell"
if t.Type == model.BuyType {
dir, cls = "BUY", "dir-buy"
}
label := productLabels[t.Product]
fmt.Fprintf(w, `<tr>
<td><span class="trade-ticker">%s</span></td>
<td>%s</td>
<td>%s</td>
<td>%s</td>
<td class="%s">%s</td>
<td>%s</td>
<td>%s</td>
</tr>`, t.Symbol, label, t.CurrencyCode,
t.Date.Format("2006-01-02"), cls, dir,
fmtNum(float64(t.Shares)), fmtNum(t.Price))
}
}
}
func TradeCount(svc *service.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
trades, err := svc.GetTrades()
if err != nil {
fmt.Fprint(w, "—")
return
}
count := len(trades)
if count == 1 {
fmt.Fprint(w, "1 trade")
} else {
fmt.Fprintf(w, "%d trades", count)
}
}
}