moving handlers into service. adding website
This commit is contained in:
239
internal/website/main.go
Normal file
239
internal/website/main.go
Normal file
@@ -0,0 +1,239 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user