Home

mint @f3cdde75f2c5333136ba1f0aeb7f88b271ba60a9 - refs - log -
-
https://git.jolheiser.com/mint.git
Budget
mint / main.go
- raw -
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package main

import (
	_ "embed"
	"encoding/json"
	"flag"
	"fmt"
	"html/template"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"time"

	"github.com/peterbourgon/ff/v3"
	"go.jolheiser.com/ffjsonnet"
)

var (
	//go:embed index.html
	_indexTmpl string
	indexTmpl  = template.Must(template.New("").Parse(_indexTmpl))
	//go:embed budget.json
	_json []byte
)

type Event struct {
	ID            string         `json:"id"`
	AllDay        bool           `json:"allDay"`
	Start         time.Time      `json:"start"`
	Title         string         `json:"title"`
	ExtendedProps map[string]any `json:"extendedProps"`
}

type Args struct {
	Port     int
	Database string
	JSON     bool
	LogLevel slog.Level
}

func maine() error {
	args := Args{
		LogLevel: slog.LevelInfo,
	}
	fs := flag.NewFlagSet("mint", flag.ExitOnError)
	fs.IntVar(&args.Port, "port", 8080, "Port to run on")
	fs.IntVar(&args.Port, "p", args.Port, "--port")
	fs.StringVar(&args.Database, "database", "mint.sqlite3", "Path to database file")
	fs.StringVar(&args.Database, "d", args.Database, "--database")
	fs.BoolVar(&args.JSON, "json", false, "JSON logging")
	fs.BoolVar(&args.JSON, "j", args.JSON, "--json")
	logFn := func(s string) error {
		switch strings.ToLower(s) {
		case "debug":
			args.LogLevel = slog.LevelDebug
		case "info":
			args.LogLevel = slog.LevelInfo
		case "warn", "warning":
			args.LogLevel = slog.LevelWarn
		case "error":
			args.LogLevel = slog.LevelError
		default:
			return fmt.Errorf("unknown log level %q: valid settings are debug, info, warn, or error")
		}
		return nil
	}
	fs.Func("log-level", "Logging level [debug, info, warn, error]", logFn)
	fs.Func("l", "--log-level", logFn)
	fs.String("config", "mint.jsonnet", "Config file")

	if err := ff.Parse(fs, os.Args[1:],
		ff.WithConfigFileFlag("config"),
		ff.WithAllowMissingConfigFile(true),
		ff.WithConfigFileParser(ffjsonnet.Parser),
		ff.WithEnvVarPrefix("MINT"),
	); err != nil {
		return fmt.Errorf("could not parse CLI arguments: %w", err)
	}

	logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
	if args.JSON {
		logger = slog.New(slog.NewJSONHandler(os.Stderr, nil))
	}
	slog.SetDefault(logger)
	slog.SetLogLoggerLevel(args.LogLevel)

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		indexTmpl.Execute(w, nil)
	})
	mux.HandleFunc("/data", func(w http.ResponseWriter, r *http.Request) {
		var budget []struct {
			Title  string
			Amount float64
			Date   time.Time
		}
		if err := json.Unmarshal(_json, &budget); err != nil {
			slog.Error("could not unmarshal budget", slog.Any("err", err))
		}
		var events []Event
		var id int
		for _, b := range budget {
			events = append(events, Event{
				ID:     strconv.Itoa(id),
				AllDay: true,
				Start:  b.Date,
				Title:  fmt.Sprintf("%s ($%.2f)", b.Title, b.Amount),
				ExtendedProps: map[string]any{
					"title":     b.Title,
					"amount":    int(b.Amount * 100),
					"recurring": true,
				},
			})
			id++
		}
		w.Header().Set("Content-Type", "application/json")
		if err := json.NewEncoder(w).Encode(events); err != nil {
			slog.Error("could not marshal budget data", slog.Any("err", err))
		}
	})

	go func() {
		addr := fmt.Sprintf(":%d", *portFlag)
		slog.Debug(fmt.Sprintf("Listening at http://localhost%s", addr))
		if err := http.ListenAndServe(addr, mux); err != nil {
			panic(err)
		}
	}()

	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Kill, os.Interrupt)
	<-ch

	return nil
}

func main() {
	if err := maine(); err != nil {
		slog.Error("error during runtime", slog.Any("err", err))
	}
}