Function bodies 204 total
main function · go · L48-L90 (43 LOC)cmd/boefetch/main.go
func main() {
outFile := flag.String("o", "boe_rates.csv", "output CSV file path")
flag.Parse()
entries, err := fetchFromAPI()
if err != nil {
fmt.Fprintf(os.Stderr, "API fetch failed (%v), using fallback data\n", err)
entries = parseFallback()
}
// Prepend the initial rate at 2020-01-01 (0.75%)
start := rateEntry{
date: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
rate: 0.75,
}
entries = append([]rateEntry{start}, entries...)
// Sort by date
sort.Slice(entries, func(i, j int) bool { return entries[i].date.Before(entries[j].date) })
// Deduplicate by date
var deduped []rateEntry
seen := map[string]bool{}
for _, e := range entries {
key := e.date.Format("2006-01-02")
if !seen[key] {
seen[key] = true
deduped = append(deduped, e)
}
}
// Write CSV
var sb strings.Builder
for _, e := range deduped {
fmt.Fprintf(&sb, "%s,%.4f\n", e.date.Format("2006-01-02"), e.rate/100.0)
}
if err := os.WriteFile(*outFile, []byte(sb.String()), 0644); err != nilparseAPIResponse function · go · L114-L154 (41 LOC)cmd/boefetch/main.go
func parseAPIResponse(body string) ([]rateEntry, error) {
lines := strings.Split(strings.TrimSpace(body), "\n")
var entries []rateEntry
for i, line := range lines {
if i == 0 {
continue // skip header
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.SplitN(line, ",", 2)
if len(parts) != 2 {
continue
}
dateStr := strings.TrimSpace(parts[0])
rateStr := strings.TrimSpace(parts[1])
t, err := time.Parse("02/Jan/2006", dateStr)
if err != nil {
// Try alternative format
t, err = time.Parse("02 Jan 2006", dateStr)
if err != nil {
continue
}
}
var rate float64
_, err = fmt.Sscanf(rateStr, "%f", &rate)
if err != nil {
continue
}
entries = append(entries, rateEntry{date: t, rate: rate})
}
if len(entries) == 0 {
return nil, fmt.Errorf("no valid entries parsed from API response")
}
return entries, nil
}parseFallback function · go · L156-L159 (4 LOC)cmd/boefetch/main.go
func parseFallback() []rateEntry {
entries, _ := parseAPIResponse(fallbackData)
return entries
}customerData function · go · L23-L32 (10 LOC)cmd/chartcompare/data.go
func customerData() []Point {
pts := make([]Point, 50)
for i := range pts {
pts[i] = Point{
Date: startDate.AddDate(0, 0, i),
Value: 5 + float64(i) + 2*math.Sin(float64(i)/5),
}
}
return pts
}balanceData function · go · L35-L46 (12 LOC)cmd/chartcompare/data.go
func balanceData() []DualPoint {
pts := make([]DualPoint, 50)
for i := range pts {
day := float64(i)
pts[i] = DualPoint{
Date: startDate.AddDate(0, 0, i),
A: 10000 + day*200 + 500*math.Sin(day/7), // savings
B: 5000 + day*100 + 300*math.Cos(day/10), // lending
}
}
return pts
}nimData function · go · L49-L59 (11 LOC)cmd/chartcompare/data.go
func nimData() []Point {
pts := make([]Point, 50)
for i := range pts {
day := float64(i)
pts[i] = Point{
Date: startDate.AddDate(0, 0, i),
Value: 200 + 80*math.Sin(day/4) + 30*math.Cos(day/7),
}
}
return pts
}boeRateData function · go · L62-L91 (30 LOC)cmd/chartcompare/data.go
func boeRateData() []Point {
steps := []struct {
dayOffset int
rate float64
}{
{0, 0.0010},
{8, 0.0025},
{16, 0.0050},
{22, 0.0100},
{28, 0.0175},
{32, 0.0250},
{36, 0.0350},
{40, 0.0425},
{44, 0.0500},
{47, 0.0525},
}
pts := make([]Point, 50)
stepIdx := 0
for i := range pts {
if stepIdx+1 < len(steps) && i >= steps[stepIdx+1].dayOffset {
stepIdx++
}
pts[i] = Point{
Date: startDate.AddDate(0, 0, i),
Value: steps[stepIdx].rate,
}
}
return pts
}About: code-quality intelligence by Repobility · https://repobility.com
thinLabels function · go · L9-L22 (14 LOC)cmd/chartcompare/gocharts.go
func thinLabels(labels []string, max int) []string {
if len(labels) <= max {
return labels
}
out := make([]string, len(labels))
copy(out, labels)
step := len(out) / max
for i := range out {
if i%step != 0 && i != len(out)-1 {
out[i] = ""
}
}
return out
}gcCustomerCount function · go · L24-L53 (30 LOC)cmd/chartcompare/gocharts.go
func gcCustomerCount(data []Point) string {
values := make([]float64, len(data))
labels := make([]string, len(data))
for i, p := range data {
values[i] = p.Value
labels[i] = p.Date.Format("2 Jan")
}
labels = thinLabels(labels, 6)
p, err := charts.LineRender(
[][]float64{values},
charts.SVGOutputOptionFunc(),
charts.DimensionsOptionFunc(svgWidth, svgHeight),
charts.XAxisLabelsOptionFunc(labels),
charts.LegendOptionFunc(charts.LegendOption{Show: charts.Ptr(false)}),
charts.PaddingOptionFunc(charts.Box{Left: 60, Right: 10, Top: 10, Bottom: 10, IsSet: true}),
func(opt *charts.ChartOption) {
opt.Symbol = charts.SymbolNone
opt.LineStrokeWidth = 2
},
)
if err != nil {
return fmt.Sprintf(`<p class="has-text-danger">Chart error: %v</p>`, err)
}
b, err := p.Bytes()
if err != nil {
return fmt.Sprintf(`<p class="has-text-danger">Render error: %v</p>`, err)
}
return string(b)
}gcBalances function · go · L55-L89 (35 LOC)cmd/chartcompare/gocharts.go
func gcBalances(data []DualPoint) string {
savings := make([]float64, len(data))
lending := make([]float64, len(data))
labels := make([]string, len(data))
for i, p := range data {
savings[i] = p.A
lending[i] = p.B
labels[i] = p.Date.Format("2 Jan")
}
labels = thinLabels(labels, 6)
p, err := charts.LineRender(
[][]float64{savings, lending},
charts.SVGOutputOptionFunc(),
charts.DimensionsOptionFunc(svgWidth, svgHeight+25),
charts.XAxisLabelsOptionFunc(labels),
charts.LegendOptionFunc(charts.LegendOption{
Show: charts.Ptr(true),
SeriesNames: []string{"Savings", "Lending"},
}),
charts.PaddingOptionFunc(charts.Box{Left: 60, Right: 10, Top: 10, Bottom: 10, IsSet: true}),
func(opt *charts.ChartOption) {
opt.Symbol = charts.SymbolNone
opt.LineStrokeWidth = 2
},
)
if err != nil {
return fmt.Sprintf(`<p class="has-text-danger">Chart error: %v</p>`, err)
}
b, err := p.Bytes()
if err != nil {
return fmt.Sprintf(`<p class="has-text-danger"gcNIM function · go · L91-L120 (30 LOC)cmd/chartcompare/gocharts.go
func gcNIM(data []Point) string {
values := make([]float64, len(data))
labels := make([]string, len(data))
for i, p := range data {
values[i] = p.Value
labels[i] = p.Date.Format("2 Jan")
}
labels = thinLabels(labels, 6)
p, err := charts.LineRender(
[][]float64{values},
charts.SVGOutputOptionFunc(),
charts.DimensionsOptionFunc(svgWidth, svgHeight),
charts.XAxisLabelsOptionFunc(labels),
charts.LegendOptionFunc(charts.LegendOption{Show: charts.Ptr(false)}),
charts.PaddingOptionFunc(charts.Box{Left: 60, Right: 10, Top: 10, Bottom: 10, IsSet: true}),
func(opt *charts.ChartOption) {
opt.Symbol = charts.SymbolNone
opt.LineStrokeWidth = 2
},
)
if err != nil {
return fmt.Sprintf(`<p class="has-text-danger">Chart error: %v</p>`, err)
}
b, err := p.Bytes()
if err != nil {
return fmt.Sprintf(`<p class="has-text-danger">Render error: %v</p>`, err)
}
return string(b)
}gcBoERate function · go · L122-L152 (31 LOC)cmd/chartcompare/gocharts.go
func gcBoERate(data []Point) string {
// Convert rate to percentage for display
values := make([]float64, len(data))
labels := make([]string, len(data))
for i, p := range data {
values[i] = p.Value * 100
labels[i] = p.Date.Format("2 Jan")
}
labels = thinLabels(labels, 6)
p, err := charts.LineRender(
[][]float64{values},
charts.SVGOutputOptionFunc(),
charts.DimensionsOptionFunc(svgWidth, svgHeight),
charts.XAxisLabelsOptionFunc(labels),
charts.LegendOptionFunc(charts.LegendOption{Show: charts.Ptr(false)}),
charts.PaddingOptionFunc(charts.Box{Left: 60, Right: 10, Top: 10, Bottom: 10, IsSet: true}),
func(opt *charts.ChartOption) {
opt.Symbol = charts.SymbolNone
opt.LineStrokeWidth = 2
},
)
if err != nil {
return fmt.Sprintf(`<p class="has-text-danger">Chart error: %v</p>`, err)
}
b, err := p.Bytes()
if err != nil {
return fmt.Sprintf(`<p class="has-text-danger">Render error: %v</p>`, err)
}
return string(b)
}hrSingleLine function · go · L21-L250 (230 LOC)cmd/chartcompare/handrolled.go
func hrSingleLine(data []Point, color, yFmt string) string {
if len(data) == 0 {
return ""
}
minV, maxV := data[0].Value, data[0].Value
for _, p := range data {
if p.Value < minV {
minV = p.Value
}
if p.Value > maxV {
maxV = p.Value
}
}
vRange := maxV - minV
if vRange < 1e-9 {
vRange = 1
minV -= 0.5
} else {
minV -= vRange * 0.1
maxV += vRange * 0.1
vRange = maxV - minV
}
if minV < 0 {
minV = 0
vRange = maxV - minV
}
var s strings.Builder
s.WriteString(fmt.Sprintf(`<svg viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" style="width:100%%;height:auto">`, svgWidth, svgHeight))
s.WriteString(`<style>text{font-family:Arial,Helvetica,sans-serif}</style>`)
// Background
s.WriteString(fmt.Sprintf(`<rect x="%d" y="%d" width="%d" height="%d" fill="#fafafa" stroke="#dbdbdb" stroke-width="1"/>`,
chartPadL, chartPadT, chartW, chartH))
// Y-axis
for i := 0; i <= 4; i++ {
val := minV + vRange*float64(i)/4.0
y := float64(chartPadT+chartH)xLabels function · go · L253-L267 (15 LOC)cmd/chartcompare/handrolled.go
func xLabels(data []Point, s *strings.Builder) {
if len(data) < 2 {
return
}
numLabels := 5
if len(data) < numLabels {
numLabels = len(data)
}
for i := 0; i < numLabels; i++ {
idx := i * (len(data) - 1) / (numLabels - 1)
x := float64(chartPadL) + float64(chartW)*float64(idx)/float64(len(data)-1)
s.WriteString(fmt.Sprintf(`<text x="%.0f" y="%d" text-anchor="middle" font-size="9" fill="#7a7a7a">%s</text>`,
x, chartPadT+chartH+15, data[idx].Date.Format("2 Jan")))
}
}hrCustomerCount function · go · L271-L273 (3 LOC)cmd/chartcompare/handrolled.go
func hrCustomerCount(data []Point) string {
return hrSingleLine(data, "#00947e", "%.0f")
}Repobility · open methodology · https://repobility.com/research/
hrBalances function · go · L275-L277 (3 LOC)cmd/chartcompare/handrolled.go
func hrBalances(data []DualPoint) string {
return hrDualLine(data, "#48c78e", "#3e8ed0", "Savings", "Lending")
}hrNIM function · go · L279-L281 (3 LOC)cmd/chartcompare/handrolled.go
func hrNIM(data []Point) string {
return hrSingleLine(data, "#f59e0b", "%.0f")
}hrBoERate function · go · L283-L285 (3 LOC)cmd/chartcompare/handrolled.go
func hrBoERate(data []Point) string {
return hrStepLine(data, "#48c78e", "%.2f%%")
}main function · go · L10-L120 (111 LOC)cmd/chartcompare/main.go
func main() {
// Find repo root (walk up from executable location)
outDir := filepath.Join("..", "..", "docs", "research")
if err := os.MkdirAll(outDir, 0o755); err != nil {
fmt.Fprintf(os.Stderr, "mkdir: %v\n", err)
os.Exit(1)
}
// Generate datasets
custData := customerData()
balData := balanceData()
nimPts := nimData()
boePts := boeRateData()
// Chart sections: title, 3 SVGs (hand-rolled, go-charts, margaid)
type section struct {
title string
handrolled, gochart, mar string
}
sections := []section{
{
title: "Customer Count",
handrolled: hrCustomerCount(custData),
gochart: gcCustomerCount(custData),
mar: mgCustomerCount(custData),
},
{
title: "Balance History (dual line)",
handrolled: hrBalances(balData),
gochart: gcBalances(balData),
mar: mgBalances(balData),
},
{
title: "NIM (bps)",
handrolled: hrNIM(nimPts),
gochart: gcNIM(nimPts),
mar: mgNIM(nimPts)mgSingleLine function · go · L10-L33 (24 LOC)cmd/chartcompare/margaid.go
func mgSingleLine(data []Point, title string) string {
series := m.NewSeries(m.Titled(title))
for _, p := range data {
series.Add(m.MakeValue(m.SecondsFromTime(p.Date), p.Value))
}
diagram := m.New(svgWidth, svgHeight,
m.WithAutorange(m.XAxis, series),
m.WithAutorange(m.YAxis, series),
m.WithInset(60),
m.WithColorScheme(160),
)
diagram.Line(series, m.UsingStrokeWidth(2))
diagram.Axis(series, m.XAxis, diagram.TimeTicker("2 Jan"), false, "")
diagram.Axis(series, m.YAxis, diagram.ValueTicker('f', 0, 10), true, "")
diagram.Frame()
var buf bytes.Buffer
if err := diagram.Render(&buf); err != nil {
return fmt.Sprintf(`<p class="has-text-danger">margaid error: %v</p>`, err)
}
return buf.String()
}mgDualLine function · go · L35-L63 (29 LOC)cmd/chartcompare/margaid.go
func mgDualLine(data []DualPoint, labelA, labelB string) string {
seriesA := m.NewSeries(m.Titled(labelA))
seriesB := m.NewSeries(m.Titled(labelB))
for _, p := range data {
t := m.SecondsFromTime(p.Date)
seriesA.Add(m.MakeValue(t, p.A))
seriesB.Add(m.MakeValue(t, p.B))
}
diagram := m.New(svgWidth, svgHeight+25,
m.WithAutorange(m.XAxis, seriesA),
m.WithAutorange(m.YAxis, seriesA, seriesB),
m.WithInset(60),
m.WithColorScheme(160),
)
diagram.Line(seriesA, m.UsingStrokeWidth(2))
diagram.Line(seriesB, m.UsingStrokeWidth(2))
diagram.Axis(seriesA, m.XAxis, diagram.TimeTicker("2 Jan"), false, "")
diagram.Axis(seriesA, m.YAxis, diagram.ValueTicker('f', 0, 10), true, "")
diagram.Frame()
diagram.Legend(m.BottomLeft)
var buf bytes.Buffer
if err := diagram.Render(&buf); err != nil {
return fmt.Sprintf(`<p class="has-text-danger">margaid error: %v</p>`, err)
}
return buf.String()
}mgStepLine function · go · L66-L94 (29 LOC)cmd/chartcompare/margaid.go
func mgStepLine(data []Point, title string) string {
series := m.NewSeries(m.Titled(title))
for i, p := range data {
t := m.SecondsFromTime(p.Date)
if i > 0 {
// Horizontal segment: previous value at current x
series.Add(m.MakeValue(t, data[i-1].Value*100))
}
series.Add(m.MakeValue(t, p.Value*100))
}
diagram := m.New(svgWidth, svgHeight,
m.WithAutorange(m.XAxis, series),
m.WithAutorange(m.YAxis, series),
m.WithInset(60),
m.WithColorScheme(160),
)
diagram.Line(series, m.UsingStrokeWidth(2))
diagram.Axis(series, m.XAxis, diagram.TimeTicker("2 Jan"), false, "")
diagram.Axis(series, m.YAxis, diagram.ValueTicker('f', 2, 10), true, "")
diagram.Frame()
var buf bytes.Buffer
if err := diagram.Render(&buf); err != nil {
return fmt.Sprintf(`<p class="has-text-danger">margaid error: %v</p>`, err)
}
return buf.String()
}mgCustomerCount function · go · L96-L98 (3 LOC)cmd/chartcompare/margaid.go
func mgCustomerCount(data []Point) string {
return mgSingleLine(data, "Customers")
}Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
mgBalances function · go · L100-L102 (3 LOC)cmd/chartcompare/margaid.go
func mgBalances(data []DualPoint) string {
return mgDualLine(data, "Savings", "Lending")
}mgNIM function · go · L104-L106 (3 LOC)cmd/chartcompare/margaid.go
func mgNIM(data []Point) string {
return mgSingleLine(data, "NIM (bps)")
}mgBoERate function · go · L108-L110 (3 LOC)cmd/chartcompare/margaid.go
func mgBoERate(data []Point) string {
return mgStepLine(data, "BoE Rate")
}BuildRuntimeHTML method · go · L13-L101 (89 LOC)cmd/demo/about.go
func (ds *DemoState) BuildRuntimeHTML() string {
var m runtime.MemStats
runtime.ReadMemStats(&m)
ds.mu.Lock()
currentDay := ds.currentDay.Format("2 Jan 2006")
dayCount := ds.dayCount
customerCount := len(ds.customers)
productCount := len(ds.products)
paymentCount := len(ds.payments)
boeRate := ds.settings.BoEBaseRate * 100
piiCount := ds.custStoreCount()
var dbStats sql.DBStats
if ds.db != nil {
dbStats = ds.db.Stats()
}
ds.mu.Unlock()
var s strings.Builder
s.WriteString(`<h2 class="title is-4">Runtime</h2>`)
s.WriteString(`<p class="subtitle is-6 has-text-grey">Runtime stats for the Model Bank simulation</p>`)
// Environment
s.WriteString(`<div class="box">`)
s.WriteString(`<h3 class="title is-5">Environment</h3>`)
s.WriteString(`<table class="table is-fullwidth">`)
s.WriteString(fmt.Sprintf(`<tr><th>Version</th><td>%s</td></tr>`, version))
s.WriteString(fmt.Sprintf(`<tr><th>Runtime</th><td>%s</td></tr>`, runtimeEnv))
s.WriteString(fmt.Sprintf(`<tr><th>GoformatBytes function · go · L103-L114 (12 LOC)cmd/demo/about.go
func formatBytes(b uint64) string {
const unit = 1024
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := uint64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMG"[exp])
}BuildPnLHTML method · go · L11-L57 (47 LOC)cmd/demo/accounting.go
func (ds *DemoState) BuildPnLHTML() string {
ds.mu.Lock()
dayCount := ds.dayCount
opCostPerDay := ds.opCostPerDay
loanInterestIncome := 0.0
depositInterestExpense := 0.0
for _, c := range ds.customers {
for _, a := range c.Accounts {
if a.Family == gbp.FamilyLending {
loanInterestIncome += a.Interest
} else {
depositInterestExpense += a.Interest
}
}
}
boeInterestIncome := ds.boeInterestAccum
ds.mu.Unlock()
opCosts := opCostPerDay * float64(dayCount)
netInterest := loanInterestIncome + boeInterestIncome - depositInterestExpense
profit := netInterest - opCosts
var s strings.Builder
s.WriteString(`<h2 class="title is-4">Profit & Loss</h2>`)
s.WriteString(fmt.Sprintf(`<p class="subtitle is-6 has-text-grey">For %d simulated days</p>`, dayCount))
s.WriteString(`<div class="box">`)
s.WriteString(`<table class="table is-fullwidth">`)
s.WriteString(`<tbody>`)
s.WriteString(fmt.Sprintf(`<tr><td><strong>Loan Interest Income</strong></td><td class="hLabel method · go · L19-L32 (14 LOC)cmd/demo/auth.go
func (r Role) Label() string {
switch r {
case RoleAdmin:
return "Admin"
case RoleAuditor:
return "Auditor"
case RoleCustomerService:
return "Customer Service"
case RoleReadOnly:
return "Read Only"
default:
return "Admin"
}
}Can method · go · L35-L52 (18 LOC)cmd/demo/auth.go
func (r Role) Can(action string) bool {
switch action {
case "sim_controls":
return r == RoleAdmin
case "settings":
return r == RoleAdmin
case "export":
return r == RoleAdmin
case "send_payment":
return r == RoleAdmin || r == RoleCustomerService
case "view_pii":
return r != RoleReadOnly
case "buy_gilt":
return r == RoleAdmin
default:
return false
}
}Repobility analyzer · published findings · https://repobility.com
ValidRole function · go · L55-L61 (7 LOC)cmd/demo/auth.go
func ValidRole(s string) bool {
switch Role(s) {
case RoleAdmin, RoleAuditor, RoleCustomerService, RoleReadOnly:
return true
}
return false
}NewAuthStore function · go · L75-L80 (6 LOC)cmd/demo/auth.go
func NewAuthStore(ttl time.Duration) *AuthStore {
return &AuthStore{
sessions: make(map[string]*sessionData),
ttl: ttl,
}
}getOrCreate method · go · L82-L89 (8 LOC)cmd/demo/auth.go
func (as *AuthStore) getOrCreate(sessionID string) *sessionData {
sd, ok := as.sessions[sessionID]
if !ok {
sd = &sessionData{Role: RoleAdmin}
as.sessions[sessionID] = sd
}
return sd
}Authorize method · go · L91-L96 (6 LOC)cmd/demo/auth.go
func (as *AuthStore) Authorize(sessionID string) {
as.mu.Lock()
sd := as.getOrCreate(sessionID)
sd.PIIExpiry = time.Now().Add(as.ttl)
as.mu.Unlock()
}Revoke method · go · L98-L104 (7 LOC)cmd/demo/auth.go
func (as *AuthStore) Revoke(sessionID string) {
as.mu.Lock()
if sd, ok := as.sessions[sessionID]; ok {
sd.PIIExpiry = time.Time{}
}
as.mu.Unlock()
}IsAuthorized method · go · L106-L117 (12 LOC)cmd/demo/auth.go
func (as *AuthStore) IsAuthorized(sessionID string) bool {
as.mu.Lock()
defer as.mu.Unlock()
sd, ok := as.sessions[sessionID]
if !ok {
return false
}
if sd.PIIExpiry.IsZero() || time.Now().After(sd.PIIExpiry) {
return false
}
return true
}SetRole method · go · L119-L124 (6 LOC)cmd/demo/auth.go
func (as *AuthStore) SetRole(sessionID string, role Role) {
as.mu.Lock()
sd := as.getOrCreate(sessionID)
sd.Role = role
as.mu.Unlock()
}GetRole method · go · L126-L134 (9 LOC)cmd/demo/auth.go
func (as *AuthStore) GetRole(sessionID string) Role {
as.mu.Lock()
defer as.mu.Unlock()
sd, ok := as.sessions[sessionID]
if !ok {
return RoleAdmin
}
return sd.Role
}About: code-quality intelligence by Repobility · https://repobility.com
EffectivePII method · go · L138-L156 (19 LOC)cmd/demo/auth.go
func (as *AuthStore) EffectivePII(sessionID string) bool {
as.mu.Lock()
defer as.mu.Unlock()
sd, ok := as.sessions[sessionID]
if !ok {
return false
}
switch sd.Role {
case RoleAuditor, RoleCustomerService:
return true
case RoleReadOnly:
return false
default: // Admin
if sd.PIIExpiry.IsZero() || time.Now().After(sd.PIIExpiry) {
return false
}
return true
}
}registerBankAppAPI function · go · L14-L90 (77 LOC)cmd/demo/bankapp_api.go
func registerBankAppAPI(state *DemoState) {
http.HandleFunc("/api/customers", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(state.bankAppCustomerList())
})
http.HandleFunc("/api/customer/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse: /api/customer/{id}/accounts or /api/customer/{id}/transactions
path := strings.TrimPrefix(r.URL.Path, "/api/customer/")
parts := strings.SplitN(path, "/", 2)
if len(parts) == 0 || parts[0] == "" {
http.NotFound(w, r)
return
}
custID := parts[0]
sub := ""
if len(parts) > 1 {
sub = parts[1]
}
w.Header().Set("Content-Type", "application/json")
switch {
case sub == "accounts":
resp := state.bankAppAccounts(custID)
bankAppCustomerList method · go · L66-L80 (15 LOC)cmd/demo/bankapp_data.go
func (ds *DemoState) bankAppCustomerList() []apiCustomer {
ds.mu.Lock()
customers := make([]CustomerRecord, len(ds.customers))
copy(customers, ds.customers)
ds.mu.Unlock()
result := make([]apiCustomer, len(customers))
for i, c := range customers {
result[i] = apiCustomer{
ID: c.ID,
Name: ds.lookupName(c.ID),
}
}
return result
}bankAppAccounts method · go · L82-L117 (36 LOC)cmd/demo/bankapp_data.go
func (ds *DemoState) bankAppAccounts(custID string) *apiAccountsResponse {
ds.mu.Lock()
var cust *CustomerRecord
for i := range ds.customers {
if ds.customers[i].ID == custID {
c := ds.customers[i]
cust = &c
break
}
}
ds.mu.Unlock()
if cust == nil {
return nil
}
resp := &apiAccountsResponse{
CustomerID: cust.ID,
CustomerName: ds.lookupName(cust.ID),
}
for _, a := range cust.Accounts {
resp.Accounts = append(resp.Accounts, apiAccount{
ProductName: a.ProductName,
Family: string(a.Family),
Rate: a.Rate,
Balance: a.Balance,
Interest: a.Interest,
})
if a.Family == gbp.FamilySavings {
resp.TotalSavings += a.Balance
} else {
resp.TotalLending += a.Balance
}
}
return resp
}bankAppTransactions method · go · L119-L140 (22 LOC)cmd/demo/bankapp_data.go
func (ds *DemoState) bankAppTransactions(custID string, page int) apiTransactionsResponse {
entries, total := ds.CustomerTransactions(custID, page, txPerPage)
apiEntries := make([]apiTxEntry, len(entries))
for i, tx := range entries {
apiEntries[i] = apiTxEntry{
ID: tx.ID,
Date: tx.Date.Format("2006-01-02"),
ProductName: tx.ProductName,
Type: tx.Type.String(),
Amount: tx.Amount,
Balance: tx.Balance,
Reference: tx.Reference,
}
}
return apiTransactionsResponse{
CustomerID: custID,
Page: page,
TotalCount: total,
PerPage: txPerPage,
Entries: apiEntries,
}
}bankAppProductDetail method · go · L142-L172 (31 LOC)cmd/demo/bankapp_data.go
func (ds *DemoState) bankAppProductDetail(custID string, accountIdx int) *apiProductDetail {
ds.mu.Lock()
var cust *CustomerRecord
for i := range ds.customers {
if ds.customers[i].ID == custID {
c := ds.customers[i]
cust = &c
break
}
}
ds.mu.Unlock()
if cust == nil || accountIdx < 0 || accountIdx >= len(cust.Accounts) {
return nil
}
a := cust.Accounts[accountIdx]
return &apiProductDetail{
CustomerID: cust.ID,
CustomerName: ds.lookupName(cust.ID),
AccountIndex: accountIdx,
ProductName: a.ProductName,
Family: string(a.Family),
Rate: a.Rate,
Balance: a.Balance,
Interest: a.Interest,
SortCode: a.SortCode,
AccountNum: a.AccountNum,
OpenDate: a.OpenDate.Format("2006-01-02"),
}
}bankAppProductTransactions method · go · L174-L195 (22 LOC)cmd/demo/bankapp_data.go
func (ds *DemoState) bankAppProductTransactions(custID string, accountIdx, page int) apiTransactionsResponse {
entries, total := ds.ProductTransactions(custID, accountIdx, page, txPerPage)
apiEntries := make([]apiTxEntry, len(entries))
for i, tx := range entries {
apiEntries[i] = apiTxEntry{
ID: tx.ID,
Date: tx.Date.Format("2006-01-02"),
ProductName: tx.ProductName,
Type: tx.Type.String(),
Amount: tx.Amount,
Balance: tx.Balance,
Reference: tx.Reference,
}
}
return apiTransactionsResponse{
CustomerID: custID,
Page: page,
TotalCount: total,
PerPage: txPerPage,
Entries: apiEntries,
}
}registerBankAppRoutes function · go · L15-L91 (77 LOC)cmd/demo/bankapp.go
func registerBankAppRoutes(state *DemoState, appCtrl *lofigui.Controller) {
appPage := func(w http.ResponseWriter, content string) {
appCtrl.RenderTemplate(w, pongo2.Context{
"results": content,
})
}
http.HandleFunc("/app/", func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/app/")
// POST /app/login — simulated login redirect
if path == "login" && r.Method == "POST" {
r.ParseForm()
custID := r.FormValue("customer_id")
if custID == "" {
http.Redirect(w, r, "/app/", http.StatusSeeOther)
return
}
http.Redirect(w, r, "/app/customer/"+custID, http.StatusSeeOther)
return
}
if r.Method != "GET" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// /app/ — login screen
if path == "" {
appPage(w, state.buildAppLoginHTML())
return
}
// /app/customer/{id}[/sub...]
if !strings.HasPrefix(path, "customer/") {
http.NotFound(w, r)
return
}
rest := strings.Repobility · open methodology · https://repobility.com/research/
phoneHeader function · go · L9-L18 (10 LOC)cmd/demo/bankapp_render.go
func phoneHeader(title, subtitle string) string {
var s strings.Builder
s.WriteString(`<div class="phone-header">`)
fmt.Fprintf(&s, `<p class="title">%s</p>`, title)
if subtitle != "" {
fmt.Fprintf(&s, `<p class="subtitle">%s</p>`, subtitle)
}
s.WriteString(`</div>`)
return s.String()
}phoneNav function · go · L21-L41 (21 LOC)cmd/demo/bankapp_render.go
func phoneNav(custID, activeTab string) string {
var s strings.Builder
s.WriteString(`<div class="phone-nav">`)
tabs := []struct {
Icon, Label, Href, Key string
}{
{"🏠", "Home", "/app/", "home"},
{"💳", "Accounts", fmt.Sprintf("/app/customer/%s", custID), "accounts"},
{"📄", "Activity", fmt.Sprintf("/app/customer/%s/transactions", custID), "transactions"},
}
for _, t := range tabs {
cls := ""
if t.Key == activeTab {
cls = ` class="is-active"`
}
fmt.Fprintf(&s, `<a href="%s"%s><span>%s</span>%s</a>`, t.Href, cls, t.Icon, t.Label)
}
s.WriteString(`</div>`)
return s.String()
}buildAppLoginHTML method · go · L45-L79 (35 LOC)cmd/demo/bankapp_render.go
func (ds *DemoState) buildAppLoginHTML() string {
customers := ds.bankAppCustomerList()
var s strings.Builder
s.WriteString(`<div class="phone-header" style="padding:24px 16px 20px;text-align:center">`)
s.WriteString(`<div style="width:56px;height:56px;border-radius:50%;background:rgba(255,255,255,0.2);margin:0 auto 8px;display:flex;align-items:center;justify-content:center;font-size:1.8rem">🏦</div>`)
s.WriteString(`<p class="title" style="font-size:1.3rem">Model Bank</p>`)
s.WriteString(`<p class="subtitle" style="font-size:0.75rem">Internet Banking</p>`)
s.WriteString(`</div>`)
s.WriteString(`<div class="phone-body">`)
if len(customers) == 0 {
s.WriteString(`<p style="color:#7a7a7a;text-align:center;margin-top:2rem">No customers yet. Start the simulation in the admin dashboard.</p>`)
} else {
// Quick-access customer cards (skip login form in WASM — just show customer list)
limit := len(customers)
if limit > 50 {
limit = 50
}
for _, c := range custopage 1 / 5next ›