240 lines
6.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|