package main import ( "bytes" "encoding/json" "fmt" "html/template" "log" "math" "net/http" "time" bolt "go.etcd.io/bbolt" ) const BucketKeyMoments = "moments" const DBFilename = "./moments.db" var indexPageTemplate = ` Add moments of your day

Remember moments: N minutes since your last memo

{{ if .Moments }}
{{ range .Moments }} {{ end }}
{{ (.Time.Format "15:04") }} {{ if ne .Diff 0 }}{{ .Diff }}m{{ else }}start{{ end }} {{ .Memo }}
{{ end }} ` // Moment is the main information this servers remembers type Moment struct { Key string Time time.Time Diff int64 Memo string } // main is the main function func main() { fmt.Println("Starting tracking backend server") path := DBFilename db, err := bolt.Open(path, 0666, nil) if err != nil { log.Println(err) return } defer db.Close() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { sess, err := NewSession(w, r) if err != nil { log.Printf("Error loading session: %s", err) return } defer sess.Flush() moments, err := loadMoments(db, time.Now().Format("2006-01-02")) if err != nil { log.Println(err) } type indexPageInfo struct { Moments []Moment LastMomentSeconds int64 } indexPage := indexPageInfo{Moments: moments} if len(moments) > 0 { a := moments for i := len(a)/2 - 1; i >= 0; i-- { opp := len(a) - 1 - i a[i], a[opp] = a[opp], a[i] } lastMoment := moments[0] indexPage.LastMomentSeconds = lastMoment.Time.Unix() } t, err := template.New("index").Parse(indexPageTemplate) if err != nil { log.Println(err) return } err = t.Execute(w, indexPage) if err != nil { log.Println(err) return } }) http.HandleFunc("/moment", func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() sess, err := NewSession(w, r) if err != nil{ log.Println(err) return } defer sess.Flush() if r.Method == http.MethodGet { moments, err := loadMoments(db, "") if err != nil { log.Println(err) } err = json.NewEncoder(w).Encode(&moments) if err != nil { log.Println(err) } } else if r.Method == http.MethodPost { // save input values err := r.ParseForm() if err != nil { log.Println(err) return } memo := r.FormValue("memo") timestamp := time.Now() err = saveMemo(db, timestamp, memo) if err != nil { log.Println(err) } http.Redirect(w, r, "/", http.StatusFound) } else { http.Error(w, "Method Not Allowed", 405) return } }) log.Fatal(http.ListenAndServe(":8096", nil)) } // loadMoments loads the moments with today as the prefix of the key from the database func loadMoments(db *bolt.DB, today string) ([]Moment, error) { var moments []Moment err := db.View(func(tx *bolt.Tx) error { // Assume bucket exists and has keys b := tx.Bucket([]byte(BucketKeyMoments)) if b == nil { // no bucket found, so moments should be empty return nil } c := b.Cursor() prefix := []byte(today) var prevTime time.Time for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() { var moment Moment err := json.Unmarshal(v, &moment) if err != nil { return err } if prevTime.IsZero() { moment.Diff = 0 } else { d := float64(moment.Time.Sub(prevTime)) / 1000000000.0 / 60.0 moment.Diff = int64(math.Ceil(d)) } prevTime = moment.Time moments = append(moments, moment) } // TODO(peter): if there are no moments, here we can add a moment for the start of the day return nil }) return moments, err } // saveMemo saves one memo to the database, it automatically generates a key // based on the timestamp func saveMemo(db *bolt.DB, timestamp time.Time, memo string) error { err := db.Update(func(tx *bolt.Tx) error { bucket, err := tx.CreateBucketIfNotExists([]byte(BucketKeyMoments)) if err != nil { return err } key := timestamp.Format(time.RFC3339) m := Moment{ Key: key, Memo: memo, Time: timestamp, } buf, err := json.Marshal(m) if err != nil { return err } return bucket.Put([]byte(m.Key), buf) }) return err }