The attodo.app, uhh... app.

goldstar

+212 -15
+5 -1
.gitignore
··· 33 33 server 34 34 attodo-dev 35 35 36 - bin/ 36 + bin/ 37 + 38 + data/app.db 39 + data/app.db-shm 40 + data/app.db-wal
+28
cmd/server/main.go
··· 15 15 "github.com/shindakun/attodo/internal/jobs" 16 16 "github.com/shindakun/attodo/internal/middleware" 17 17 "github.com/shindakun/attodo/internal/push" 18 + stripeClient "github.com/shindakun/attodo/internal/stripe" 19 + "github.com/shindakun/attodo/internal/supporter" 18 20 ) 19 21 20 22 func main() { ··· 34 36 35 37 // Initialize repositories 36 38 notificationRepo := database.NewNotificationRepo(db) 39 + supporterRepo := database.NewSupporterRepo(db) 40 + 41 + // Initialize services 42 + supporterService := supporter.NewService(supporterRepo) 37 43 38 44 // Initialize handlers 39 45 authHandler := handlers.NewAuthHandler(cfg) ··· 42 48 listHandler := handlers.NewListHandler(authHandler.Client()) 43 49 settingsHandler := handlers.NewSettingsHandler(authHandler.Client()) 44 50 pushHandler := handlers.NewPushHandler(notificationRepo) 51 + 52 + // Initialize Stripe client and supporter handler (only if Stripe keys are configured) 53 + var supporterHandler *handlers.SupporterHandler 54 + if cfg.StripeSecretKey != "" && cfg.StripePriceID != "" { 55 + stripe := stripeClient.NewClient(cfg.StripeSecretKey, cfg.StripeWebhookSecret, cfg.StripePriceID) 56 + supporterHandler = handlers.NewSupporterHandler(supporterService, stripe, cfg.BaseURL) 57 + log.Println("Stripe integration initialized") 58 + } else { 59 + log.Println("Stripe keys not configured - supporter features disabled") 60 + } 45 61 46 62 // Wire up cross-references between handlers 47 63 taskHandler.SetListHandler(listHandler) ··· 146 162 logRoute("POST /app/push/test [protected]") 147 163 mux.Handle("/app/push/check", authMiddleware.RequireAuth(http.HandlerFunc(pushHandler.HandleCheckTasks))) 148 164 logRoute("POST /app/push/check [protected]") 165 + 166 + // Supporter routes (only if Stripe is configured) 167 + if supporterHandler != nil { 168 + mux.Handle("/supporter/status", authMiddleware.RequireAuth(http.HandlerFunc(supporterHandler.HandleGetStatus))) 169 + logRoute("GET /supporter/status [protected]") 170 + mux.Handle("/supporter/checkout", authMiddleware.RequireAuth(http.HandlerFunc(supporterHandler.HandleCreateCheckoutSession))) 171 + logRoute("GET /supporter/checkout [protected]") 172 + mux.Handle("/supporter/portal", authMiddleware.RequireAuth(http.HandlerFunc(supporterHandler.HandleCreatePortalSession))) 173 + logRoute("GET /supporter/portal [protected]") 174 + mux.HandleFunc("/supporter/webhook", supporterHandler.HandleStripeWebhook) 175 + logRoute("POST /supporter/webhook [public - webhook]") 176 + } 149 177 150 178 // Log all registered routes 151 179 log.Println("Registered routes:")
+3
go.mod
··· 3 3 go 1.25.4 4 4 5 5 require ( 6 + github.com/SherClockHolmes/webpush-go v1.4.0 6 7 github.com/bluesky-social/indigo v0.0.0-20251029012702-8c31d8b88187 7 8 github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a 8 9 github.com/joho/godotenv v1.5.1 10 + github.com/mattn/go-sqlite3 v1.14.32 9 11 github.com/shindakun/bskyoauth v1.4.2 12 + github.com/stripe/stripe-go/v84 v84.0.0 10 13 ) 11 14 12 15 require (
+66
go.sum
··· 1 1 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 + github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s= 3 + github.com/SherClockHolmes/webpush-go v1.4.0/go.mod h1:XSq8pKX11vNV8MJEMwjrlTkxhAj1zKfxmyhdV7Pd6UA= 2 4 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 3 5 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 4 6 github.com/bluesky-social/indigo v0.0.0-20251029012702-8c31d8b88187 h1:qLP5xM4nuPfSNEAouQmXcNK2XkH+zzhfNcZMytjBodw= ··· 23 25 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 24 26 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 25 27 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 28 + github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 26 29 github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= 27 30 github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 28 31 github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a h1:l7A0loSszR5zHd/qK53ZIHMO8b3bBSmENnQ6eKnUT0A= 29 32 github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= 33 + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 30 34 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 31 35 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 32 36 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= ··· 93 97 github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 94 98 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 95 99 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 100 + github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= 101 + github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 96 102 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 97 103 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 98 104 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= ··· 142 148 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 143 149 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 144 150 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 151 + github.com/stripe/stripe-go/v84 v84.0.0 h1:4bZvf5DVdfnvgBDnW/PB24N2LwDFBVwguMB4khAZ+KI= 152 + github.com/stripe/stripe-go/v84 v84.0.0/go.mod h1:kjXh3OrF4PT16qz7z9Q5yqYAZ1mJmu8g8f4Z1sOHBfc= 145 153 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 146 154 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 147 155 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= ··· 149 157 github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 150 158 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 151 159 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 160 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 152 161 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA= 153 162 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8= 154 163 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q= ··· 187 196 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 188 197 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 189 198 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 199 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 200 + golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 201 + golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 202 + golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 203 + golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 190 204 golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= 191 205 golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 192 206 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 193 207 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 194 208 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 195 209 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 210 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 211 + golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 212 + golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 213 + golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 214 + golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 196 215 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 197 216 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 198 217 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 199 218 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 200 219 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 220 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 221 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 222 + golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 223 + golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 224 + golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 225 + golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 226 + golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 227 + golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= 228 + golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 201 229 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 202 230 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 203 231 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 232 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 233 + golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 234 + golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 235 + golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 236 + golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 237 + golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 204 238 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 205 239 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 206 240 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 241 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 242 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 244 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 207 246 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 247 + golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 248 + golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 249 + golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 250 + golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 251 + golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 208 252 golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 209 253 golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 254 + golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 255 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 256 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 257 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 258 + golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 259 + golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 260 + golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 261 + golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 262 + golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 210 263 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 211 264 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 265 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 266 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 267 + golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 268 + golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 269 + golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 270 + golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 271 + golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 272 + golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= 273 + golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 212 274 golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 213 275 golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 214 276 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 220 282 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 221 283 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 222 284 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 285 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 286 + golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 287 + golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 288 + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 223 289 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 224 290 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 225 291 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+110 -14
static/sw.js
··· 1 1 // Service Worker for AT Todo 2 - const CACHE_NAME = 'attodo-v1'; 2 + const CACHE_NAME = 'attodo-v4'; // Added server ping for periodic checks 3 3 const HEALTH_CHECK_INTERVAL = 60000; // 60 seconds 4 4 5 5 // Install event - cache essential resources ··· 37 37 38 38 // Fetch event - network first, fall back to cache 39 39 self.addEventListener('fetch', (event) => { 40 + // For non-GET requests, just pass through to network without caching 41 + if (event.request.method !== 'GET') { 42 + event.respondWith(fetch(event.request)); 43 + return; 44 + } 45 + 46 + // For GET requests: network first, fall back to cache 40 47 event.respondWith( 41 48 fetch(event.request) 42 49 .then((response) => { 43 - // Only cache GET requests (Cache API doesn't support PUT, POST, DELETE, etc.) 44 - if (event.request.method === 'GET') { 45 - const responseToCache = response.clone(); 46 - caches.open(CACHE_NAME).then((cache) => { 47 - cache.put(event.request, responseToCache); 48 - }); 49 - } 50 + // Cache successful GET responses 51 + const responseToCache = response.clone(); 52 + caches.open(CACHE_NAME).then((cache) => { 53 + cache.put(event.request, responseToCache); 54 + }); 50 55 return response; 51 56 }) 52 57 .catch(() => { 53 - // If network fails, try cache (only works for GET requests) 58 + // If network fails, try cache 54 59 return caches.match(event.request); 55 60 }) 56 61 ); ··· 65 70 }); 66 71 67 72 if (response.ok) { 73 + // Verify content type before parsing 74 + const contentType = response.headers.get('content-type'); 75 + if (!contentType || !contentType.includes('application/json')) { 76 + console.error('Health check returned non-JSON response:', contentType); 77 + throw new Error('Invalid content-type for health check'); 78 + } 79 + 68 80 const data = await response.json(); 69 81 console.log('Health check passed:', data); 70 82 ··· 129 141 // NOTIFICATION SYSTEM 130 142 // ============================================================================ 131 143 144 + // Handle push notifications 145 + self.addEventListener('push', (event) => { 146 + console.log('[Push] Push notification received:', event); 147 + 148 + // Default notification data 149 + let notificationData = { 150 + title: 'AT Todo', 151 + body: 'You have a new notification', 152 + icon: '/static/icon-192.png', 153 + badge: '/static/icon-192.png', 154 + }; 155 + 156 + // Parse the notification payload if present 157 + if (event.data) { 158 + try { 159 + const payload = event.data.json(); 160 + console.log('[Push] Payload:', payload); 161 + 162 + notificationData = { 163 + title: payload.title || notificationData.title, 164 + body: payload.body || notificationData.body, 165 + icon: payload.icon || notificationData.icon, 166 + badge: payload.badge || notificationData.badge, 167 + tag: payload.tag, 168 + data: payload.data, 169 + }; 170 + } catch (err) { 171 + console.error('[Push] Failed to parse notification payload:', err); 172 + } 173 + } 174 + 175 + // Show the notification 176 + event.waitUntil( 177 + self.registration.showNotification(notificationData.title, { 178 + body: notificationData.body, 179 + icon: notificationData.icon, 180 + badge: notificationData.badge, 181 + tag: notificationData.tag, 182 + data: notificationData.data, 183 + vibrate: [200, 100, 200], 184 + }) 185 + ); 186 + }); 187 + 132 188 // Notification permission state 133 189 let notificationsEnabled = false; 134 190 ··· 140 196 // Check for due tasks periodically 141 197 self.addEventListener('periodicsync', (event) => { 142 198 if (event.tag === 'check-due-tasks') { 143 - event.waitUntil(checkDueTasksAndNotify()); 199 + event.waitUntil( 200 + Promise.all([ 201 + checkDueTasksAndNotify(), // Client-side notification display 202 + pingServerForCheck() // Ping server (for future server-side push) 203 + ]) 204 + ); 144 205 } 145 206 }); 146 207 147 208 // Handle background sync 148 209 self.addEventListener('sync', (event) => { 149 210 if (event.tag === 'check-tasks') { 150 - event.waitUntil(checkDueTasksAndNotify()); 211 + event.waitUntil( 212 + Promise.all([ 213 + checkDueTasksAndNotify(), 214 + pingServerForCheck() 215 + ]) 216 + ); 151 217 } 152 218 }); 153 219 220 + // Ping server for notification check (for future server-side checking) 221 + async function pingServerForCheck() { 222 + try { 223 + await fetch('/app/push/check', { 224 + method: 'POST', 225 + credentials: 'include' 226 + }); 227 + } catch (err) { 228 + // Silently fail - server may not be available 229 + console.log('[Push] Server check ping failed:', err.message); 230 + } 231 + } 232 + 154 233 // Handle notification clicks 155 234 self.addEventListener('notificationclick', (event) => { 156 235 event.notification.close(); ··· 221 300 // Get notification settings from AT Protocol (with caching) 222 301 const settings = await getSettings(); 223 302 if (!settings) { 303 + console.warn('[Notifications] Failed to get settings'); 224 304 return; // Failed to get settings 225 305 } 226 306 ··· 236 316 : (hour >= quietStart && hour < quietEnd); 237 317 238 318 if (isQuiet) { 319 + console.log('[Notifications] Quiet hours active, skipping'); 239 320 return; 240 321 } 241 322 } 242 323 243 324 // Fetch tasks as JSON 244 325 const tasksResponse = await fetch('/app/tasks?filter=incomplete&format=json', { 245 - credentials: 'include' 326 + credentials: 'include', 327 + headers: { 328 + 'Accept': 'application/json' 329 + } 246 330 }); 247 331 248 332 if (!tasksResponse.ok) { 333 + console.error('[Notifications] Failed to fetch tasks:', tasksResponse.status, tasksResponse.statusText); 334 + return; 335 + } 336 + 337 + // Verify content type 338 + const contentType = tasksResponse.headers.get('content-type'); 339 + if (!contentType || !contentType.includes('application/json')) { 340 + console.error('[Notifications] Tasks endpoint returned non-JSON response:', contentType); 341 + const text = await tasksResponse.text(); 342 + console.error('[Notifications] Response body (first 200 chars):', text.substring(0, 200)); 249 343 return; 250 344 } 251 345 252 346 const data = await tasksResponse.json(); 253 - const tasks = data.tasks || data; // Handle both {tasks: []} and [] formats 347 + const tasks = Array.isArray(data) ? data : (data.tasks || []); 348 + console.log(`[Notifications] Fetched ${tasks.length} tasks`) 254 349 255 350 // Group tasks by notification type 256 351 const groups = { ··· 287 382 // Send grouped notifications 288 383 await sendGroupedNotifications(groups, settings); 289 384 } catch (error) { 290 - // Error checking tasks for notifications 385 + console.error('[Notifications] Error checking tasks:', error); 386 + console.error('[Notifications] Stack trace:', error.stack); 291 387 } 292 388 } 293 389