Recently Written · git

sbm-sync

Sync server for sbm bookmark files: one small Go program, plain files, AGPL

git clone https://github.com/equwal/sbm-sync

Log | Files | Refs


commit 5117da8633af46002d89a255a3d3edb067deea72
equwal <truex@equwal.com>
2026-09-22 18:33:33 -0700

Merge master: the billing API for the add-on, and the terms of sale

The routes of both branches stay. The account page keeps the state
text of master (server.state) and also sets page.Active for the link
to the bookmarks.

 README.md      |  29 +++++++++-
 main.go        | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++------
 pages.html     |  36 ++++++++++++-
 server_test.go | 131 ++++++++++++++++++++++++++++++++++++++++++++-
 stripe.go      |  16 +++---
 5 files changed, 348 insertions(+), 28 deletions(-)
diff --cc main.go
index a7b992f,8084f8a..890ddda
--- a/main.go
+++ b/main.go
@@@ -163,14 -167,9 +167,17 @@@ func (s *server) routes() http.Handler 
  	m.HandleFunc("POST /api/login", s.apiLogin)
  	m.HandleFunc("POST /api/logout", s.apiLogout)
  	m.HandleFunc("POST /api/sync", s.sync)
+ 	m.HandleFunc("GET /api/account", s.apiAccount)
+ 	m.HandleFunc("POST /api/checkout", s.apiCheckout)
+ 	m.HandleFunc("POST /api/portal", s.apiPortal)
 +	m.HandleFunc("GET /bookmarks", s.bookmarks)
 +	m.HandleFunc("POST /bookmarks/add", s.addBookmark)
 +	m.HandleFunc("GET /bookmarks/edit", s.editBookmark)
 +	m.HandleFunc("POST /bookmarks/edit", s.saveBookmark)
 +	m.HandleFunc("GET /bookmarks/file", s.bookmarkFile)
 +	m.HandleFunc("POST /bookmarks/file", s.saveFile)
 +	m.HandleFunc("POST /bookmarks/import", s.importFile)
 +	m.HandleFunc("GET /bookmarks.txt", s.download)
  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  		h := w.Header()
  		h.Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; "+
@@@ -431,7 -433,13 +444,14 @@@ func (s *server) showAccount(w http.Res
  	p := s.page("Your account")
  	p.Email, p.Error, p.Customer = a.Email, problem, a.Customer
  	p.Unconfirmed = !s.verified(a)
 +	p.Active = s.verified(a) && s.active(a)
+ 	p.State = s.state(a)
+ 	p.CanSubscribe = s.stripe != nil && !paid(a.Status)
+ 	s.render(w, status, "account", p)
+ }
+ 
+ // state tells in one sentence if sync works for the account, and why.
+ func (s *server) state(a Account) string {
  	left := s.trialLeft(a)
  	switch {
  	case s.stripe == nil: