diff --git a/main.go b/main.go new file mode 100644 index 0000000..46b9fc9 --- /dev/null +++ b/main.go @@ -0,0 +1,254 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "time" + + bolt "go.etcd.io/bbolt" +) + +const BucketKeyMoments = "moments" + +var indexPageTemplate = ` + + + + + Add moments of your day + + + + +
+
+

Remember moments: N minutes since your last memo

+ +
+
+ +
+
+
+
+ +
+
+
+
+
+{{ if .Moments }} +
+
+ {{ range .Moments }} +
+
+ {{ (.Time.Format "15:04") }} + {{ .Memo }} +
+
+ {{ end }} +
+
+ +{{ end }} + + + ` + +// Moment is the main information this servers remembers +type Moment struct { + Key string + Time time.Time + Memo string +} + +func main() { + fmt.Println("Starting tracking backend server") + + path := "./moments.db" + 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) { + 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() + + 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)) +} + +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) + 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 + } + moments = append(moments, moment) + } + + return nil + }) + + return moments, err +} + +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 +}