tangled
alpha
login
or
join now
shindakun.net
/
attodo.app
3
fork
atom
The attodo.app, uhh... app.
3
fork
atom
overview
issues
pulls
pipelines
goldstar
Steve Layton
3 months ago
f4e7b903
c82fbae2
+212
-15
5 changed files
expand all
collapse all
unified
split
.gitignore
cmd
server
main.go
go.mod
go.sum
static
sw.js
+5
-1
.gitignore
···
33
33
server
34
34
attodo-dev
35
35
36
36
-
bin/
36
36
+
bin/
37
37
+
38
38
+
data/app.db
39
39
+
data/app.db-shm
40
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
18
+
stripeClient "github.com/shindakun/attodo/internal/stripe"
19
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
39
+
supporterRepo := database.NewSupporterRepo(db)
40
40
+
41
41
+
// Initialize services
42
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
51
+
52
52
+
// Initialize Stripe client and supporter handler (only if Stripe keys are configured)
53
53
+
var supporterHandler *handlers.SupporterHandler
54
54
+
if cfg.StripeSecretKey != "" && cfg.StripePriceID != "" {
55
55
+
stripe := stripeClient.NewClient(cfg.StripeSecretKey, cfg.StripeWebhookSecret, cfg.StripePriceID)
56
56
+
supporterHandler = handlers.NewSupporterHandler(supporterService, stripe, cfg.BaseURL)
57
57
+
log.Println("Stripe integration initialized")
58
58
+
} else {
59
59
+
log.Println("Stripe keys not configured - supporter features disabled")
60
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
165
+
166
166
+
// Supporter routes (only if Stripe is configured)
167
167
+
if supporterHandler != nil {
168
168
+
mux.Handle("/supporter/status", authMiddleware.RequireAuth(http.HandlerFunc(supporterHandler.HandleGetStatus)))
169
169
+
logRoute("GET /supporter/status [protected]")
170
170
+
mux.Handle("/supporter/checkout", authMiddleware.RequireAuth(http.HandlerFunc(supporterHandler.HandleCreateCheckoutSession)))
171
171
+
logRoute("GET /supporter/checkout [protected]")
172
172
+
mux.Handle("/supporter/portal", authMiddleware.RequireAuth(http.HandlerFunc(supporterHandler.HandleCreatePortalSession)))
173
173
+
logRoute("GET /supporter/portal [protected]")
174
174
+
mux.HandleFunc("/supporter/webhook", supporterHandler.HandleStripeWebhook)
175
175
+
logRoute("POST /supporter/webhook [public - webhook]")
176
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
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
10
+
github.com/mattn/go-sqlite3 v1.14.32
9
11
github.com/shindakun/bskyoauth v1.4.2
12
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
2
+
github.com/SherClockHolmes/webpush-go v1.4.0 h1:ocnzNKWN23T9nvHi6IfyrQjkIc0oJWv1B1pULsf9i3s=
3
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
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
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
100
+
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
101
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
151
+
github.com/stripe/stripe-go/v84 v84.0.0 h1:4bZvf5DVdfnvgBDnW/PB24N2LwDFBVwguMB4khAZ+KI=
152
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
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
199
+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
200
200
+
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
201
201
+
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
202
202
+
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
203
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
210
+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
211
211
+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
212
212
+
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
213
213
+
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
214
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
220
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
221
221
+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
222
222
+
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
223
223
+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
224
224
+
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
225
225
+
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
226
226
+
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
227
227
+
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
228
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
232
+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
233
233
+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
234
234
+
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
235
235
+
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
236
236
+
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
237
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
241
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
242
242
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
243
243
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
244
244
+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
245
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
247
+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
248
248
+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
249
249
+
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
250
250
+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
251
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
254
+
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
255
255
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
256
256
+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
257
257
+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
258
258
+
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
259
259
+
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
260
260
+
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
261
261
+
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
262
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
265
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
266
266
+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
267
267
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
268
268
+
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
269
269
+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
270
270
+
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
271
271
+
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
272
272
+
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
273
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
285
+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
286
286
+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
287
287
+
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
288
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
2
-
const CACHE_NAME = 'attodo-v1';
2
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
40
+
// For non-GET requests, just pass through to network without caching
41
41
+
if (event.request.method !== 'GET') {
42
42
+
event.respondWith(fetch(event.request));
43
43
+
return;
44
44
+
}
45
45
+
46
46
+
// For GET requests: network first, fall back to cache
40
47
event.respondWith(
41
48
fetch(event.request)
42
49
.then((response) => {
43
43
-
// Only cache GET requests (Cache API doesn't support PUT, POST, DELETE, etc.)
44
44
-
if (event.request.method === 'GET') {
45
45
-
const responseToCache = response.clone();
46
46
-
caches.open(CACHE_NAME).then((cache) => {
47
47
-
cache.put(event.request, responseToCache);
48
48
-
});
49
49
-
}
50
50
+
// Cache successful GET responses
51
51
+
const responseToCache = response.clone();
52
52
+
caches.open(CACHE_NAME).then((cache) => {
53
53
+
cache.put(event.request, responseToCache);
54
54
+
});
50
55
return response;
51
56
})
52
57
.catch(() => {
53
53
-
// If network fails, try cache (only works for GET requests)
58
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
73
+
// Verify content type before parsing
74
74
+
const contentType = response.headers.get('content-type');
75
75
+
if (!contentType || !contentType.includes('application/json')) {
76
76
+
console.error('Health check returned non-JSON response:', contentType);
77
77
+
throw new Error('Invalid content-type for health check');
78
78
+
}
79
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
144
+
// Handle push notifications
145
145
+
self.addEventListener('push', (event) => {
146
146
+
console.log('[Push] Push notification received:', event);
147
147
+
148
148
+
// Default notification data
149
149
+
let notificationData = {
150
150
+
title: 'AT Todo',
151
151
+
body: 'You have a new notification',
152
152
+
icon: '/static/icon-192.png',
153
153
+
badge: '/static/icon-192.png',
154
154
+
};
155
155
+
156
156
+
// Parse the notification payload if present
157
157
+
if (event.data) {
158
158
+
try {
159
159
+
const payload = event.data.json();
160
160
+
console.log('[Push] Payload:', payload);
161
161
+
162
162
+
notificationData = {
163
163
+
title: payload.title || notificationData.title,
164
164
+
body: payload.body || notificationData.body,
165
165
+
icon: payload.icon || notificationData.icon,
166
166
+
badge: payload.badge || notificationData.badge,
167
167
+
tag: payload.tag,
168
168
+
data: payload.data,
169
169
+
};
170
170
+
} catch (err) {
171
171
+
console.error('[Push] Failed to parse notification payload:', err);
172
172
+
}
173
173
+
}
174
174
+
175
175
+
// Show the notification
176
176
+
event.waitUntil(
177
177
+
self.registration.showNotification(notificationData.title, {
178
178
+
body: notificationData.body,
179
179
+
icon: notificationData.icon,
180
180
+
badge: notificationData.badge,
181
181
+
tag: notificationData.tag,
182
182
+
data: notificationData.data,
183
183
+
vibrate: [200, 100, 200],
184
184
+
})
185
185
+
);
186
186
+
});
187
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
143
-
event.waitUntil(checkDueTasksAndNotify());
199
199
+
event.waitUntil(
200
200
+
Promise.all([
201
201
+
checkDueTasksAndNotify(), // Client-side notification display
202
202
+
pingServerForCheck() // Ping server (for future server-side push)
203
203
+
])
204
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
150
-
event.waitUntil(checkDueTasksAndNotify());
211
211
+
event.waitUntil(
212
212
+
Promise.all([
213
213
+
checkDueTasksAndNotify(),
214
214
+
pingServerForCheck()
215
215
+
])
216
216
+
);
151
217
}
152
218
});
153
219
220
220
+
// Ping server for notification check (for future server-side checking)
221
221
+
async function pingServerForCheck() {
222
222
+
try {
223
223
+
await fetch('/app/push/check', {
224
224
+
method: 'POST',
225
225
+
credentials: 'include'
226
226
+
});
227
227
+
} catch (err) {
228
228
+
// Silently fail - server may not be available
229
229
+
console.log('[Push] Server check ping failed:', err.message);
230
230
+
}
231
231
+
}
232
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
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
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
245
-
credentials: 'include'
326
326
+
credentials: 'include',
327
327
+
headers: {
328
328
+
'Accept': 'application/json'
329
329
+
}
246
330
});
247
331
248
332
if (!tasksResponse.ok) {
333
333
+
console.error('[Notifications] Failed to fetch tasks:', tasksResponse.status, tasksResponse.statusText);
334
334
+
return;
335
335
+
}
336
336
+
337
337
+
// Verify content type
338
338
+
const contentType = tasksResponse.headers.get('content-type');
339
339
+
if (!contentType || !contentType.includes('application/json')) {
340
340
+
console.error('[Notifications] Tasks endpoint returned non-JSON response:', contentType);
341
341
+
const text = await tasksResponse.text();
342
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
253
-
const tasks = data.tasks || data; // Handle both {tasks: []} and [] formats
347
347
+
const tasks = Array.isArray(data) ? data : (data.tasks || []);
348
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
290
-
// Error checking tasks for notifications
385
385
+
console.error('[Notifications] Error checking tasks:', error);
386
386
+
console.error('[Notifications] Stack trace:', error.stack);
291
387
}
292
388
}
293
389