max / goingson
- Co-Authored-By
- Claude Opus 4.6 <noreply@anthropic.com>
31 files changed,
+729 insertions,
-374 deletions
| @@ -414,22 +414,13 @@ | |||
| 414 | 414 | "generic-array", | |
| 415 | 415 | ] | |
| 416 | 416 | ||
| 417 | - | [[package]] | |
| 418 | - | name = "block2" | |
| 419 | - | version = "0.5.1" | |
| 420 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 421 | - | checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" | |
| 422 | - | dependencies = [ | |
| 423 | - | "objc2 0.5.2", | |
| 424 | - | ] | |
| 425 | - | ||
| 426 | 417 | [[package]] | |
| 427 | 418 | name = "block2" | |
| 428 | 419 | version = "0.6.2" | |
| 429 | 420 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 430 | 421 | checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" | |
| 431 | 422 | dependencies = [ | |
| 432 | - | "objc2 0.6.3", | |
| 423 | + | "objc2", | |
| 433 | 424 | ] | |
| 434 | 425 | ||
| 435 | 426 | [[package]] | |
| @@ -1082,9 +1073,9 @@ | |||
| 1082 | 1073 | checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" | |
| 1083 | 1074 | dependencies = [ | |
| 1084 | 1075 | "bitflags 2.10.0", | |
| 1085 | - | "block2 0.6.2", | |
| 1076 | + | "block2", | |
| 1086 | 1077 | "libc", | |
| 1087 | - | "objc2 0.6.3", | |
| 1078 | + | "objc2", | |
| 1088 | 1079 | ] | |
| 1089 | 1080 | ||
| 1090 | 1081 | [[package]] | |
| @@ -1867,9 +1858,6 @@ | |||
| 1867 | 1858 | "mailparse", | |
| 1868 | 1859 | "notify", | |
| 1869 | 1860 | "notify-debouncer-mini", | |
| 1870 | - | "objc2 0.5.2", | |
| 1871 | - | "objc2-app-kit 0.2.2", | |
| 1872 | - | "objc2-foundation 0.2.2", | |
| 1873 | 1861 | "open", | |
| 1874 | 1862 | "rand 0.8.5", | |
| 1875 | 1863 | "reqwest 0.12.28", | |
| @@ -2808,8 +2796,8 @@ | |||
| 2808 | 2796 | checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" | |
| 2809 | 2797 | dependencies = [ | |
| 2810 | 2798 | "cc", | |
| 2811 | - | "objc2 0.6.3", | |
| 2812 | - | "objc2-foundation 0.3.2", | |
| 2799 | + | "objc2", | |
| 2800 | + | "objc2-foundation", | |
| 2813 | 2801 | "time", | |
| 2814 | 2802 | ] | |
| 2815 | 2803 | ||
| @@ -2988,10 +2976,10 @@ | |||
| 2988 | 2976 | "dpi", | |
| 2989 | 2977 | "gtk", | |
| 2990 | 2978 | "keyboard-types", | |
| 2991 | - | "objc2 0.6.3", | |
| 2992 | - | "objc2-app-kit 0.3.2", | |
| 2979 | + | "objc2", | |
| 2980 | + | "objc2-app-kit", | |
| 2993 | 2981 | "objc2-core-foundation", | |
| 2994 | - | "objc2-foundation 0.3.2", | |
| 2982 | + | "objc2-foundation", | |
| 2995 | 2983 | "once_cell", | |
| 2996 | 2984 | "png 0.17.16", | |
| 2997 | 2985 | "serde", | |
| @@ -3222,22 +3210,6 @@ | |||
| 3222 | 3210 | "syn 2.0.114", | |
| 3223 | 3211 | ] | |
| 3224 | 3212 | ||
| 3225 | - | [[package]] | |
| 3226 | - | name = "objc-sys" | |
| 3227 | - | version = "0.3.5" | |
| 3228 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3229 | - | checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" | |
| 3230 | - | ||
| 3231 | - | [[package]] | |
| 3232 | - | name = "objc2" | |
| 3233 | - | version = "0.5.2" | |
| 3234 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3235 | - | checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" | |
| 3236 | - | dependencies = [ | |
| 3237 | - | "objc-sys", | |
| 3238 | - | "objc2-encode", | |
| 3239 | - | ] | |
| 3240 | - | ||
| 3241 | 3213 | [[package]] | |
| 3242 | 3214 | name = "objc2" | |
| 3243 | 3215 | version = "0.6.3" | |
| @@ -3248,22 +3220,6 @@ | |||
| 3248 | 3220 | "objc2-exception-helper", | |
| 3249 | 3221 | ] | |
| 3250 | 3222 | ||
| 3251 | - | [[package]] | |
| 3252 | - | name = "objc2-app-kit" | |
| 3253 | - | version = "0.2.2" | |
| 3254 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3255 | - | checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" | |
| 3256 | - | dependencies = [ | |
| 3257 | - | "bitflags 2.10.0", | |
| 3258 | - | "block2 0.5.1", | |
| 3259 | - | "libc", | |
| 3260 | - | "objc2 0.5.2", | |
| 3261 | - | "objc2-core-data 0.2.2", | |
| 3262 | - | "objc2-core-image 0.2.2", | |
| 3263 | - | "objc2-foundation 0.2.2", | |
| 3264 | - | "objc2-quartz-core 0.2.2", | |
| 3265 | - | ] | |
| 3266 | - | ||
| 3267 | 3223 | [[package]] | |
| 3268 | 3224 | name = "objc2-app-kit" | |
| 3269 | 3225 | version = "0.3.2" | |
| @@ -3271,18 +3227,18 @@ | |||
| 3271 | 3227 | checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" | |
| 3272 | 3228 | dependencies = [ | |
| 3273 | 3229 | "bitflags 2.10.0", | |
| 3274 | - | "block2 0.6.2", | |
| 3230 | + | "block2", | |
| 3275 | 3231 | "libc", | |
| 3276 | - | "objc2 0.6.3", | |
| 3232 | + | "objc2", | |
| 3277 | 3233 | "objc2-cloud-kit", | |
| 3278 | - | "objc2-core-data 0.3.2", | |
| 3234 | + | "objc2-core-data", | |
| 3279 | 3235 | "objc2-core-foundation", | |
| 3280 | 3236 | "objc2-core-graphics", | |
| 3281 | - | "objc2-core-image 0.3.2", | |
| 3237 | + | "objc2-core-image", | |
| 3282 | 3238 | "objc2-core-text", | |
| 3283 | 3239 | "objc2-core-video", | |
| 3284 | - | "objc2-foundation 0.3.2", | |
| 3285 | - | "objc2-quartz-core 0.3.2", | |
| 3240 | + | "objc2-foundation", | |
| 3241 | + | "objc2-quartz-core", | |
| 3286 | 3242 | ] | |
| 3287 | 3243 | ||
| 3288 | 3244 | [[package]] | |
| @@ -3292,20 +3248,8 @@ | |||
| 3292 | 3248 | checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" | |
| 3293 | 3249 | dependencies = [ | |
| 3294 | 3250 | "bitflags 2.10.0", | |
| 3295 | - | "objc2 0.6.3", | |
| 3296 | - | "objc2-foundation 0.3.2", | |
| 3297 | - | ] | |
| 3298 | - | ||
| 3299 | - | [[package]] | |
| 3300 | - | name = "objc2-core-data" | |
| 3301 | - | version = "0.2.2" | |
| 3302 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3303 | - | checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" | |
| 3304 | - | dependencies = [ | |
| 3305 | - | "bitflags 2.10.0", | |
| 3306 | - | "block2 0.5.1", | |
| 3307 | - | "objc2 0.5.2", | |
| 3308 | - | "objc2-foundation 0.2.2", | |
| 3251 | + | "objc2", | |
| 3252 | + | "objc2-foundation", | |
| 3309 | 3253 | ] | |
| 3310 | 3254 | ||
| 3311 | 3255 | [[package]] | |
| @@ -3315,8 +3259,8 @@ | |||
| 3315 | 3259 | checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" | |
| 3316 | 3260 | dependencies = [ | |
| 3317 | 3261 | "bitflags 2.10.0", | |
| 3318 | - | "objc2 0.6.3", | |
| 3319 | - | "objc2-foundation 0.3.2", | |
| 3262 | + | "objc2", | |
| 3263 | + | "objc2-foundation", | |
| 3320 | 3264 | ] | |
| 3321 | 3265 | ||
| 3322 | 3266 | [[package]] | |
| @@ -3327,7 +3271,7 @@ | |||
| 3327 | 3271 | dependencies = [ | |
| 3328 | 3272 | "bitflags 2.10.0", | |
| 3329 | 3273 | "dispatch2", | |
| 3330 | - | "objc2 0.6.3", | |
| 3274 | + | "objc2", | |
| 3331 | 3275 | ] | |
| 3332 | 3276 | ||
| 3333 | 3277 | [[package]] | |
| @@ -3338,31 +3282,19 @@ | |||
| 3338 | 3282 | dependencies = [ | |
| 3339 | 3283 | "bitflags 2.10.0", | |
| 3340 | 3284 | "dispatch2", | |
| 3341 | - | "objc2 0.6.3", | |
| 3285 | + | "objc2", | |
| 3342 | 3286 | "objc2-core-foundation", | |
| 3343 | 3287 | "objc2-io-surface", | |
| 3344 | 3288 | ] | |
| 3345 | 3289 | ||
| 3346 | - | [[package]] | |
| 3347 | - | name = "objc2-core-image" | |
| 3348 | - | version = "0.2.2" | |
| 3349 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3350 | - | checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" | |
| 3351 | - | dependencies = [ | |
| 3352 | - | "block2 0.5.1", | |
| 3353 | - | "objc2 0.5.2", | |
| 3354 | - | "objc2-foundation 0.2.2", | |
| 3355 | - | "objc2-metal", | |
| 3356 | - | ] | |
| 3357 | - | ||
| 3358 | 3290 | [[package]] | |
| 3359 | 3291 | name = "objc2-core-image" | |
| 3360 | 3292 | version = "0.3.2" | |
| 3361 | 3293 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3362 | 3294 | checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" | |
| 3363 | 3295 | dependencies = [ | |
| 3364 | - | "objc2 0.6.3", | |
| 3365 | - | "objc2-foundation 0.3.2", | |
| 3296 | + | "objc2", | |
| 3297 | + | "objc2-foundation", | |
| 3366 | 3298 | ] | |
| 3367 | 3299 | ||
| 3368 | 3300 | [[package]] | |
| @@ -3372,7 +3304,7 @@ | |||
| 3372 | 3304 | checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" | |
| 3373 | 3305 | dependencies = [ | |
| 3374 | 3306 | "bitflags 2.10.0", | |
| 3375 | - | "objc2 0.6.3", | |
| 3307 | + | "objc2", | |
| 3376 | 3308 | "objc2-core-foundation", | |
| 3377 | 3309 | "objc2-core-graphics", | |
| 3378 | 3310 | ] | |
| @@ -3384,7 +3316,7 @@ | |||
| 3384 | 3316 | checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" | |
| 3385 | 3317 | dependencies = [ | |
| 3386 | 3318 | "bitflags 2.10.0", | |
| 3387 | - | "objc2 0.6.3", | |
| 3319 | + | "objc2", | |
| 3388 | 3320 | "objc2-core-foundation", | |
| 3389 | 3321 | "objc2-core-graphics", | |
| 3390 | 3322 | "objc2-io-surface", | |
| @@ -3405,18 +3337,6 @@ | |||
| 3405 | 3337 | "cc", | |
| 3406 | 3338 | ] | |
| 3407 | 3339 | ||
| 3408 | - | [[package]] | |
| 3409 | - | name = "objc2-foundation" | |
| 3410 | - | version = "0.2.2" | |
| 3411 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3412 | - | checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" | |
| 3413 | - | dependencies = [ | |
| 3414 | - | "bitflags 2.10.0", | |
| 3415 | - | "block2 0.5.1", | |
| 3416 | - | "libc", | |
| 3417 | - | "objc2 0.5.2", | |
| 3418 | - | ] | |
| 3419 | - | ||
| 3420 | 3340 | [[package]] | |
| 3421 | 3341 | name = "objc2-foundation" | |
| 3422 | 3342 | version = "0.3.2" | |
| @@ -3424,9 +3344,9 @@ | |||
| 3424 | 3344 | checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" | |
| 3425 | 3345 | dependencies = [ | |
| 3426 | 3346 | "bitflags 2.10.0", | |
| 3427 | - | "block2 0.6.2", | |
| 3347 | + | "block2", | |
| 3428 | 3348 | "libc", | |
| 3429 | - | "objc2 0.6.3", | |
| 3349 | + | "objc2", | |
| 3430 | 3350 | "objc2-core-foundation", | |
| 3431 | 3351 | ] | |
| 3432 | 3352 | ||
| @@ -3437,7 +3357,7 @@ | |||
| 3437 | 3357 | checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" | |
| 3438 | 3358 | dependencies = [ | |
| 3439 | 3359 | "bitflags 2.10.0", | |
| 3440 | - | "objc2 0.6.3", | |
| 3360 | + | "objc2", | |
| 3441 | 3361 | "objc2-core-foundation", | |
| 3442 | 3362 | ] | |
| 3443 | 3363 | ||
| @@ -3447,22 +3367,10 @@ | |||
| 3447 | 3367 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3448 | 3368 | checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" | |
| 3449 | 3369 | dependencies = [ | |
| 3450 | - | "objc2 0.6.3", | |
| 3370 | + | "objc2", | |
| 3451 | 3371 | "objc2-core-foundation", | |
| 3452 | 3372 | ] | |
| 3453 | 3373 | ||
| 3454 | - | [[package]] | |
| 3455 | - | name = "objc2-metal" | |
| 3456 | - | version = "0.2.2" | |
| 3457 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3458 | - | checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" | |
| 3459 | - | dependencies = [ | |
| 3460 | - | "bitflags 2.10.0", | |
| 3461 | - | "block2 0.5.1", | |
| 3462 | - | "objc2 0.5.2", | |
| 3463 | - | "objc2-foundation 0.2.2", | |
| 3464 | - | ] | |
| 3465 | - | ||
| 3466 | 3374 | [[package]] | |
| 3467 | 3375 | name = "objc2-osa-kit" | |
| 3468 | 3376 | version = "0.3.2" | |
| @@ -3470,22 +3378,9 @@ | |||
| 3470 | 3378 | checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" | |
| 3471 | 3379 | dependencies = [ | |
| 3472 | 3380 | "bitflags 2.10.0", | |
| 3473 | - | "objc2 0.6.3", | |
| 3474 | - | "objc2-app-kit 0.3.2", | |
| 3475 | - | "objc2-foundation 0.3.2", | |
| 3476 | - | ] | |
| 3477 | - | ||
| 3478 | - | [[package]] | |
| 3479 | - | name = "objc2-quartz-core" | |
| 3480 | - | version = "0.2.2" | |
| 3481 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3482 | - | checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" | |
| 3483 | - | dependencies = [ | |
| 3484 | - | "bitflags 2.10.0", | |
| 3485 | - | "block2 0.5.1", | |
| 3486 | - | "objc2 0.5.2", | |
| 3487 | - | "objc2-foundation 0.2.2", | |
| 3488 | - | "objc2-metal", | |
| 3381 | + | "objc2", | |
| 3382 | + | "objc2-app-kit", | |
| 3383 | + | "objc2-foundation", | |
| 3489 | 3384 | ] | |
| 3490 | 3385 | ||
| 3491 | 3386 | [[package]] | |
| @@ -3495,9 +3390,9 @@ | |||
| 3495 | 3390 | checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" | |
| 3496 | 3391 | dependencies = [ | |
| 3497 | 3392 | "bitflags 2.10.0", | |
| 3498 | - | "objc2 0.6.3", | |
| 3393 | + | "objc2", | |
| 3499 | 3394 | "objc2-core-foundation", | |
| 3500 | - | "objc2-foundation 0.3.2", | |
| 3395 | + | "objc2-foundation", | |
| 3501 | 3396 | ] | |
| 3502 | 3397 | ||
| 3503 | 3398 | [[package]] | |
| @@ -3507,7 +3402,7 @@ | |||
| 3507 | 3402 | checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" | |
| 3508 | 3403 | dependencies = [ | |
| 3509 | 3404 | "bitflags 2.10.0", | |
| 3510 | - | "objc2 0.6.3", | |
| 3405 | + | "objc2", | |
| 3511 | 3406 | "objc2-core-foundation", | |
| 3512 | 3407 | ] | |
| 3513 | 3408 | ||
| @@ -3518,9 +3413,9 @@ | |||
| 3518 | 3413 | checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" | |
| 3519 | 3414 | dependencies = [ | |
| 3520 | 3415 | "bitflags 2.10.0", | |
| 3521 | - | "objc2 0.6.3", | |
| 3416 | + | "objc2", | |
| 3522 | 3417 | "objc2-core-foundation", | |
| 3523 | - | "objc2-foundation 0.3.2", | |
| 3418 | + | "objc2-foundation", | |
| 3524 | 3419 | ] | |
| 3525 | 3420 | ||
| 3526 | 3421 | [[package]] | |
| @@ -3530,11 +3425,11 @@ | |||
| 3530 | 3425 | checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" | |
| 3531 | 3426 | dependencies = [ | |
| 3532 | 3427 | "bitflags 2.10.0", | |
| 3533 | - | "block2 0.6.2", | |
| 3534 | - | "objc2 0.6.3", | |
| 3535 | - | "objc2-app-kit 0.3.2", | |
| 3428 | + | "block2", | |
| 3429 | + | "objc2", | |
| 3430 | + | "objc2-app-kit", | |
| 3536 | 3431 | "objc2-core-foundation", | |
| 3537 | - | "objc2-foundation 0.3.2", | |
| 3432 | + | "objc2-foundation", | |
| 3538 | 3433 | "objc2-javascript-core", | |
| 3539 | 3434 | "objc2-security", | |
| 3540 | 3435 | ] | |
| @@ -3657,8 +3552,8 @@ | |||
| 3657 | 3552 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 3658 | 3553 | checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" | |
| 3659 | 3554 | dependencies = [ | |
| 3660 | - | "objc2 0.6.3", | |
| 3661 | - | "objc2-foundation 0.3.2", | |
| 3555 | + | "objc2", | |
| 3556 | + | "objc2-foundation", | |
| 3662 | 3557 | "objc2-osa-kit", | |
| 3663 | 3558 | "serde", | |
| 3664 | 3559 | "serde_json", | |
| @@ -4466,17 +4361,17 @@ | |||
| 4466 | 4361 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 4467 | 4362 | checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" | |
| 4468 | 4363 | dependencies = [ | |
| 4469 | - | "block2 0.6.2", | |
| 4364 | + | "block2", | |
| 4470 | 4365 | "dispatch2", | |
| 4471 | 4366 | "glib-sys", | |
| 4472 | 4367 | "gobject-sys", | |
| 4473 | 4368 | "gtk-sys", | |
| 4474 | 4369 | "js-sys", | |
| 4475 | 4370 | "log", | |
| 4476 | - | "objc2 0.6.3", | |
| 4477 | - | "objc2-app-kit 0.3.2", | |
| 4371 | + | "objc2", | |
| 4372 | + | "objc2-app-kit", | |
| 4478 | 4373 | "objc2-core-foundation", | |
| 4479 | - | "objc2-foundation 0.3.2", | |
| 4374 | + | "objc2-foundation", | |
| 4480 | 4375 | "raw-window-handle", | |
| 4481 | 4376 | "wasm-bindgen", | |
| 4482 | 4377 | "wasm-bindgen-futures", | |
| @@ -5161,11 +5056,11 @@ | |||
| 5161 | 5056 | "bytemuck", | |
| 5162 | 5057 | "js-sys", | |
| 5163 | 5058 | "ndk", | |
| 5164 | - | "objc2 0.6.3", | |
| 5059 | + | "objc2", | |
| 5165 | 5060 | "objc2-core-foundation", | |
| 5166 | 5061 | "objc2-core-graphics", | |
| 5167 | - | "objc2-foundation 0.3.2", | |
| 5168 | - | "objc2-quartz-core 0.3.2", | |
| 5062 | + | "objc2-foundation", | |
| 5063 | + | "objc2-quartz-core", | |
| 5169 | 5064 | "raw-window-handle", | |
| 5170 | 5065 | "redox_syscall 0.5.18", | |
| 5171 | 5066 | "tracing", | |
| @@ -5643,7 +5538,7 @@ | |||
| 5643 | 5538 | checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" | |
| 5644 | 5539 | dependencies = [ | |
| 5645 | 5540 | "bitflags 2.10.0", | |
| 5646 | - | "block2 0.6.2", | |
| 5541 | + | "block2", | |
| 5647 | 5542 | "core-foundation 0.10.1", | |
| 5648 | 5543 | "core-graphics", | |
| 5649 | 5544 | "crossbeam-channel", | |
| @@ -5660,9 +5555,9 @@ | |||
| 5660 | 5555 | "ndk", | |
| 5661 | 5556 | "ndk-context", | |
| 5662 | 5557 | "ndk-sys", | |
| 5663 | - | "objc2 0.6.3", | |
| 5664 | - | "objc2-app-kit 0.3.2", | |
| 5665 | - | "objc2-foundation 0.3.2", | |
| 5558 | + | "objc2", | |
| 5559 | + | "objc2-app-kit", | |
| 5560 | + | "objc2-foundation", | |
| 5666 | 5561 | "once_cell", | |
| 5667 | 5562 | "parking_lot", | |
| 5668 | 5563 | "raw-window-handle", | |
| @@ -5727,9 +5622,9 @@ | |||
| 5727 | 5622 | "log", | |
| 5728 | 5623 | "mime", | |
| 5729 | 5624 | "muda", | |
| 5730 | - | "objc2 0.6.3", | |
| 5731 | - | "objc2-app-kit 0.3.2", | |
| 5732 | - | "objc2-foundation 0.3.2", | |
| 5625 | + | "objc2", | |
| 5626 | + | "objc2-app-kit", | |
| 5627 | + | "objc2-foundation", | |
| 5733 | 5628 | "objc2-ui-kit", | |
| 5734 | 5629 | "objc2-web-kit", | |
| 5735 | 5630 | "percent-encoding", | |
| @@ -5975,7 +5870,7 @@ | |||
| 5975 | 5870 | "gtk", | |
| 5976 | 5871 | "http", | |
| 5977 | 5872 | "jni", | |
| 5978 | - | "objc2 0.6.3", | |
| 5873 | + | "objc2", | |
| 5979 | 5874 | "objc2-ui-kit", | |
| 5980 | 5875 | "objc2-web-kit", | |
| 5981 | 5876 | "raw-window-handle", | |
| @@ -5999,9 +5894,9 @@ | |||
| 5999 | 5894 | "http", | |
| 6000 | 5895 | "jni", | |
| 6001 | 5896 | "log", | |
| 6002 | - | "objc2 0.6.3", | |
| 6003 | - | "objc2-app-kit 0.3.2", | |
| 6004 | - | "objc2-foundation 0.3.2", | |
| 5897 | + | "objc2", | |
| 5898 | + | "objc2-app-kit", | |
| 5899 | + | "objc2-foundation", | |
| 6005 | 5900 | "once_cell", | |
| 6006 | 5901 | "percent-encoding", | |
| 6007 | 5902 | "raw-window-handle", | |
| @@ -6161,9 +6056,9 @@ | |||
| 6161 | 6056 | ||
| 6162 | 6057 | [[package]] | |
| 6163 | 6058 | name = "time" | |
| 6164 | - | version = "0.3.46" | |
| 6059 | + | version = "0.3.47" | |
| 6165 | 6060 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6166 | - | checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" | |
| 6061 | + | checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" | |
| 6167 | 6062 | dependencies = [ | |
| 6168 | 6063 | "deranged", | |
| 6169 | 6064 | "itoa", | |
| @@ -6182,9 +6077,9 @@ | |||
| 6182 | 6077 | ||
| 6183 | 6078 | [[package]] | |
| 6184 | 6079 | name = "time-macros" | |
| 6185 | - | version = "0.2.26" | |
| 6080 | + | version = "0.2.27" | |
| 6186 | 6081 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 6187 | - | checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" | |
| 6082 | + | checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" | |
| 6188 | 6083 | dependencies = [ | |
| 6189 | 6084 | "num-conv", | |
| 6190 | 6085 | "time-core", | |
| @@ -6508,11 +6403,11 @@ | |||
| 6508 | 6403 | "dirs", | |
| 6509 | 6404 | "libappindicator", | |
| 6510 | 6405 | "muda", | |
| 6511 | - | "objc2 0.6.3", | |
| 6512 | - | "objc2-app-kit 0.3.2", | |
| 6406 | + | "objc2", | |
| 6407 | + | "objc2-app-kit", | |
| 6513 | 6408 | "objc2-core-foundation", | |
| 6514 | 6409 | "objc2-core-graphics", | |
| 6515 | - | "objc2-foundation 0.3.2", | |
| 6410 | + | "objc2-foundation", | |
| 6516 | 6411 | "once_cell", | |
| 6517 | 6412 | "png 0.17.16", | |
| 6518 | 6413 | "serde", | |
| @@ -7025,10 +6920,10 @@ | |||
| 7025 | 6920 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 7026 | 6921 | checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" | |
| 7027 | 6922 | dependencies = [ | |
| 7028 | - | "objc2 0.6.3", | |
| 7029 | - | "objc2-app-kit 0.3.2", | |
| 6923 | + | "objc2", | |
| 6924 | + | "objc2-app-kit", | |
| 7030 | 6925 | "objc2-core-foundation", | |
| 7031 | - | "objc2-foundation 0.3.2", | |
| 6926 | + | "objc2-foundation", | |
| 7032 | 6927 | "raw-window-handle", | |
| 7033 | 6928 | "windows-sys 0.59.0", | |
| 7034 | 6929 | "windows-version", | |
| @@ -7546,7 +7441,7 @@ |
Lines truncated
| @@ -81,12 +81,6 @@ | |||
| 81 | 81 | ||
| 82 | 82 | # === Desktop-only dependencies (not available on iOS/Android) === | |
| 83 | 83 | ||
| 84 | - | # macOS-only: dock icon | |
| 85 | - | [target.'cfg(target_os = "macos")'.dependencies] | |
| 86 | - | objc2 = "0.5" | |
| 87 | - | objc2-app-kit = { version = "0.2", features = ["NSApplication", "NSImage", "NSResponder"] } | |
| 88 | - | objc2-foundation = { version = "0.2", features = ["NSData", "NSThread"] } | |
| 89 | - | ||
| 90 | 84 | # Desktop-only: plugins that require desktop window management | |
| 91 | 85 | [target.'cfg(not(any(target_os = "ios", target_os = "android")))'.dependencies] | |
| 92 | 86 | tauri-plugin-shell = { workspace = true } |
| @@ -26,26 +26,6 @@ | |||
| 26 | 26 | #[cfg(not(any(target_os = "ios", target_os = "android")))] | |
| 27 | 27 | use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; | |
| 28 | 28 | ||
| 29 | - | /// Set the macOS dock icon from embedded PNG data. | |
| 30 | - | /// This is needed in dev mode since the app isn't bundled. | |
| 31 | - | #[cfg(target_os = "macos")] | |
| 32 | - | fn set_dock_icon() { | |
| 33 | - | use objc2::ClassType; | |
| 34 | - | use objc2_app_kit::{NSApplication, NSImage}; | |
| 35 | - | use objc2_foundation::{MainThreadMarker, NSData}; | |
| 36 | - | ||
| 37 | - | const ICON_DATA: &[u8] = include_bytes!("../icons/icon.png"); | |
| 38 | - | ||
| 39 | - | let Some(mtm) = MainThreadMarker::new() else { | |
| 40 | - | return; | |
| 41 | - | }; | |
| 42 | - | let app = NSApplication::sharedApplication(mtm); | |
| 43 | - | let data = NSData::with_bytes(ICON_DATA); | |
| 44 | - | if let Some(image) = NSImage::initWithData(NSImage::alloc(), &data) { | |
| 45 | - | unsafe { app.setApplicationIconImage(Some(&image)) }; | |
| 46 | - | } | |
| 47 | - | } | |
| 48 | - | ||
| 49 | 29 | /// Set up the macOS menu bar tray icon showing "GO" in Reglo. | |
| 50 | 30 | #[cfg(not(any(target_os = "ios", target_os = "android")))] | |
| 51 | 31 | fn setup_tray(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> { | |
| @@ -292,10 +272,6 @@ | |||
| 292 | 272 | } | |
| 293 | 273 | }) | |
| 294 | 274 | .setup(|app| { | |
| 295 | - | // Set dock icon (needed for dev mode on macOS) | |
| 296 | - | #[cfg(target_os = "macos")] | |
| 297 | - | set_dock_icon(); | |
| 298 | - | ||
| 299 | 275 | // Set up menu bar tray icon (desktop only) | |
| 300 | 276 | #[cfg(not(any(target_os = "ios", target_os = "android")))] | |
| 301 | 277 | { |
| @@ -89,6 +89,9 @@ | |||
| 89 | 89 | // Ensure desktop user exists (single-user mode) | |
| 90 | 90 | ensure_desktop_user_exists(&pool).await?; | |
| 91 | 91 | ||
| 92 | + | // Migrate LLM API key from database to keychain (one-time) | |
| 93 | + | migrate_llm_api_key_to_keychain(&pool).await; | |
| 94 | + | ||
| 92 | 95 | // Create repositories | |
| 93 | 96 | let projects = Arc::new(SqliteProjectRepository::new(pool.clone())); | |
| 94 | 97 | let tasks = Arc::new(SqliteTaskRepository::new(pool.clone())); | |
| @@ -196,3 +199,36 @@ | |||
| 196 | 199 | ||
| 197 | 200 | Ok(()) | |
| 198 | 201 | } | |
| 202 | + | ||
| 203 | + | /// One-time migration: if LLM API key exists in the database, move it to keychain and set DB column to NULL. | |
| 204 | + | #[instrument(skip(pool))] | |
| 205 | + | async fn migrate_llm_api_key_to_keychain(pool: &SqlitePool) { | |
| 206 | + | let user_id = DESKTOP_USER_ID.to_string(); | |
| 207 | + | ||
| 208 | + | let row: Option<(Option<String>,)> = sqlx::query_as( | |
| 209 | + | "SELECT api_key FROM llm_settings WHERE user_id = ? AND api_key IS NOT NULL AND api_key != ''" | |
| 210 | + | ) | |
| 211 | + | .bind(&user_id) | |
| 212 | + | .fetch_optional(pool) | |
| 213 | + | .await | |
| 214 | + | .unwrap_or(None); | |
| 215 | + | ||
| 216 | + | if let Some((Some(db_key),)) = row { | |
| 217 | + | // Only migrate if keychain doesn't already have a key | |
| 218 | + | if crate::oauth::CredentialStore::get_llm_api_key().is_none() { | |
| 219 | + | match crate::oauth::CredentialStore::store_llm_api_key(&db_key) { | |
| 220 | + | Ok(()) => { | |
| 221 | + | // Clear the key from the database | |
| 222 | + | let _ = sqlx::query("UPDATE llm_settings SET api_key = NULL WHERE user_id = ?") | |
| 223 | + | .bind(&user_id) | |
| 224 | + | .execute(pool) | |
| 225 | + | .await; | |
| 226 | + | info!("Migrated LLM API key from database to keychain"); | |
| 227 | + | } | |
| 228 | + | Err(e) => { | |
| 229 | + | warn!("Failed to migrate LLM API key to keychain: {}", e); | |
| 230 | + | } | |
| 231 | + | } | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | } |
| @@ -22,7 +22,7 @@ | |||
| 22 | 22 | use chrono::Utc; | |
| 23 | 23 | use goingson_core::CoreError; | |
| 24 | 24 | use serde::{Deserialize, Serialize}; | |
| 25 | - | use sqlx::SqlitePool; | |
| 25 | + | use sqlx::{SqliteConnection, SqlitePool}; | |
| 26 | 26 | use synckit_client::{ChangeEntry, ChangeOp, SyncKitClient}; | |
| 27 | 27 | use tracing::{debug, info, warn}; | |
| 28 | 28 | use uuid::Uuid; | |
| @@ -30,6 +30,14 @@ | |||
| 30 | 30 | /// Maximum changes to push in a single batch. | |
| 31 | 31 | const PUSH_BATCH_LIMIT: i64 = 500; | |
| 32 | 32 | ||
| 33 | + | /// Email account columns that sync (config only — credentials stay per-device). | |
| 34 | + | const EMAIL_ACCOUNT_SYNC_COLS: &[&str] = &[ | |
| 35 | + | "id", "user_id", "account_name", "email_address", | |
| 36 | + | "imap_server", "imap_port", "smtp_server", "smtp_port", | |
| 37 | + | "username", "use_tls", "created_at", "archive_folder_name", | |
| 38 | + | "auth_type", "jmap_session_url", "jmap_account_id", "sync_interval_minutes", | |
| 39 | + | ]; | |
| 40 | + | ||
| 33 | 41 | /// Tables in FK-safe order for upserts (parents first). | |
| 34 | 42 | const UPSERT_ORDER: &[&str] = &[ | |
| 35 | 43 | "projects", | |
| @@ -229,12 +237,29 @@ | |||
| 229 | 237 | Ok(total_applied) | |
| 230 | 238 | } | |
| 231 | 239 | ||
| 232 | - | /// Apply remote changes to local DB with triggers suppressed. | |
| 240 | + | /// Apply remote changes to local DB with triggers suppressed and FK enforcement off. | |
| 241 | + | /// | |
| 242 | + | /// FK enforcement is disabled so that tasks with `source_email_id` pointing to | |
| 243 | + | /// emails not yet fetched locally can be inserted without error. Uses a dedicated | |
| 244 | + | /// connection (same pattern as `crates/db-sqlite/src/migrations.rs`). | |
| 233 | 245 | async fn apply_remote_changes(pool: &SqlitePool, changes: Vec<ChangeEntry>) -> Result<(), CoreError> { | |
| 234 | 246 | // Set applying_remote flag to suppress triggers | |
| 235 | 247 | set_sync_state(pool, "applying_remote", "1").await?; | |
| 236 | 248 | ||
| 237 | - | let result = apply_changes_inner(pool, changes).await; | |
| 249 | + | // Acquire a dedicated connection for FK pragma (pool-level pragma is per-connection) | |
| 250 | + | let mut conn = pool.acquire().await.map_err(CoreError::database)?; | |
| 251 | + | ||
| 252 | + | sqlx::query("PRAGMA foreign_keys = OFF") | |
| 253 | + | .execute(&mut *conn) | |
| 254 | + | .await | |
| 255 | + | .map_err(CoreError::database)?; | |
| 256 | + | ||
| 257 | + | let result = apply_changes_inner(&mut conn, changes).await; | |
| 258 | + | ||
| 259 | + | sqlx::query("PRAGMA foreign_keys = ON") | |
| 260 | + | .execute(&mut *conn) | |
| 261 | + | .await | |
| 262 | + | .map_err(CoreError::database)?; | |
| 238 | 263 | ||
| 239 | 264 | // Always clear the flag, even on error | |
| 240 | 265 | set_sync_state(pool, "applying_remote", "0").await?; | |
| @@ -242,7 +267,10 @@ | |||
| 242 | 267 | result | |
| 243 | 268 | } | |
| 244 | 269 | ||
| 245 | - | async fn apply_changes_inner(pool: &SqlitePool, changes: Vec<ChangeEntry>) -> Result<(), CoreError> { | |
| 270 | + | async fn apply_changes_inner( | |
| 271 | + | conn: &mut SqliteConnection, | |
| 272 | + | changes: Vec<ChangeEntry>, | |
| 273 | + | ) -> Result<(), CoreError> { | |
| 246 | 274 | // Separate upserts from deletes | |
| 247 | 275 | let mut upserts: Vec<&ChangeEntry> = Vec::new(); | |
| 248 | 276 | let mut deletes: Vec<&ChangeEntry> = Vec::new(); | |
| @@ -259,7 +287,7 @@ | |||
| 259 | 287 | for change in &upserts { | |
| 260 | 288 | if change.table == *table { | |
| 261 | 289 | if let Some(ref data) = change.data { | |
| 262 | - | apply_upsert(pool, table, &change.row_id, data).await?; | |
| 290 | + | apply_upsert(&mut *conn, table, &change.row_id, data).await?; | |
| 263 | 291 | } | |
| 264 | 292 | } | |
| 265 | 293 | } | |
| @@ -269,7 +297,7 @@ | |||
| 269 | 297 | for table in DELETE_ORDER { | |
| 270 | 298 | for change in &deletes { | |
| 271 | 299 | if change.table == *table { | |
| 272 | - | apply_delete(pool, table, &change.row_id).await?; | |
| 300 | + | apply_delete(&mut *conn, table, &change.row_id).await?; | |
| 273 | 301 | } | |
| 274 | 302 | } | |
| 275 | 303 | } | |
| @@ -312,24 +340,23 @@ | |||
| 312 | 340 | "id", "user_id", "project_id", "name", "description", "position", "target_date", | |
| 313 | 341 | "status", "created_at", | |
| 314 | 342 | ]), | |
| 315 | - | "email_accounts" => Some(&[ | |
| 316 | - | "id", "user_id", "account_name", "email_address", "imap_server", "imap_port", | |
| 317 | - | "smtp_server", "smtp_port", "username", "password", "use_tls", "created_at", | |
| 318 | - | "archive_folder_name", "auth_type", "oauth2_access_token", "oauth2_refresh_token", | |
| 319 | - | "oauth2_token_expires_at", "jmap_session_url", "jmap_account_id", | |
| 320 | - | "sync_interval_minutes", | |
| 321 | - | ]), | |
| 343 | + | "email_accounts" => Some(EMAIL_ACCOUNT_SYNC_COLS), | |
| 322 | 344 | _ => None, | |
| 323 | 345 | } | |
| 324 | 346 | } | |
| 325 | 347 | ||
| 326 | 348 | /// Apply an INSERT OR REPLACE for a remote change. | |
| 327 | 349 | async fn apply_upsert( | |
| 328 | - | pool: &SqlitePool, | |
| 350 | + | conn: &mut SqliteConnection, | |
| 329 | 351 | table: &str, | |
| 330 | 352 | _row_id: &str, | |
| 331 | 353 | data: &serde_json::Value, | |
| 332 | 354 | ) -> Result<(), CoreError> { | |
| 355 | + | // Email accounts use ON CONFLICT to preserve local credentials | |
| 356 | + | if table == "email_accounts" { | |
| 357 | + | return apply_email_account_upsert(conn, data).await; | |
| 358 | + | } | |
| 359 | + | ||
| 333 | 360 | let columns = table_columns(table) | |
| 334 | 361 | .ok_or_else(|| CoreError::bad_request(format!("unknown syncable table: {}", table)))?; | |
| 335 | 362 | ||
| @@ -343,43 +370,94 @@ | |||
| 343 | 370 | let mut query = sqlx::query(&sql); | |
| 344 | 371 | ||
| 345 | 372 | for col in columns { | |
| 346 | - | let val = &data[*col]; | |
| 347 | - | match val { | |
| 348 | - | serde_json::Value::String(s) => { | |
| 349 | - | query = query.bind(s.as_str()); | |
| 350 | - | } | |
| 351 | - | serde_json::Value::Number(n) => { | |
| 352 | - | if let Some(i) = n.as_i64() { | |
| 353 | - | query = query.bind(i); | |
| 354 | - | } else if let Some(f) = n.as_f64() { | |
| 355 | - | query = query.bind(f); | |
| 356 | - | } else { | |
| 357 | - | query = query.bind(None::<String>); | |
| 358 | - | } | |
| 359 | - | } | |
| 360 | - | serde_json::Value::Bool(b) => { | |
| 361 | - | query = query.bind(if *b { 1i32 } else { 0i32 }); | |
| 362 | - | } | |
| 363 | - | serde_json::Value::Null => { | |
| 364 | - | query = query.bind(None::<String>); | |
| 365 | - | } | |
| 366 | - | _ => { | |
| 367 | - | // Arrays/objects: serialize as JSON string | |
| 368 | - | query = query.bind(val.to_string()); | |
| 369 | - | } | |
| 370 | - | } | |
| 373 | + | query = bind_json_value(query, &data[*col]); | |
| 371 | 374 | } | |
| 372 | 375 | ||
| 373 | 376 | query | |
| 374 | - | .execute(pool) | |
| 377 | + | .execute(&mut *conn) | |
| 375 | 378 | .await | |
| 376 | 379 | .map_err(CoreError::database)?; | |
| 377 | 380 | ||
| 378 | 381 | Ok(()) | |
| 379 | 382 | } | |
| 380 | 383 | ||
| 384 | + | /// Apply an upsert for email_accounts that preserves local credentials. | |
| 385 | + | /// | |
| 386 | + | /// Uses INSERT ... ON CONFLICT(id) DO UPDATE to only touch the 16 config columns, | |
| 387 | + | /// leaving `password`, `oauth2_access_token`, `oauth2_refresh_token`, and | |
| 388 | + | /// `oauth2_token_expires_at` untouched on existing rows. New rows get `password = ''` | |
| 389 | + | /// to satisfy the NOT NULL constraint. | |
| 390 | + | async fn apply_email_account_upsert( | |
| 391 | + | conn: &mut SqliteConnection, | |
| 392 | + | data: &serde_json::Value, | |
| 393 | + | ) -> Result<(), CoreError> { | |
| 394 | + | let cols = EMAIL_ACCOUNT_SYNC_COLS; | |
| 395 | + | ||
| 396 | + | // INSERT columns: 16 sync cols + password (hardcoded to '') | |
| 397 | + | let mut insert_cols: Vec<&str> = cols.to_vec(); | |
| 398 | + | insert_cols.push("password"); | |
| 399 | + | ||
| 400 | + | let col_list = insert_cols.join(", "); | |
| 401 | + | let placeholders = insert_cols.iter().map(|_| "?").collect::<Vec<_>>().join(", "); | |
| 402 | + | ||
| 403 | + | // ON CONFLICT: only update the 16 sync columns | |
| 404 | + | let update_set = cols | |
| 405 | + | .iter() | |
| 406 | + | .filter(|c| **c != "id") | |
| 407 | + | .map(|c| format!("{} = excluded.{}", c, c)) | |
| 408 | + | .collect::<Vec<_>>() | |
| 409 | + | .join(", "); | |
| 410 | + | ||
| 411 | + | let sql = format!( | |
| 412 | + | "INSERT INTO email_accounts ({}) VALUES ({}) ON CONFLICT(id) DO UPDATE SET {}", | |
| 413 | + | col_list, placeholders, update_set | |
| 414 | + | ); | |
| 415 | + | ||
| 416 | + | let mut query = sqlx::query(&sql); | |
| 417 | + | ||
| 418 | + | // Bind the 16 sync columns from data | |
| 419 | + | for col in cols { | |
| 420 | + | query = bind_json_value(query, &data[*col]); | |
| 421 | + | } | |
| 422 | + | ||
| 423 | + | // Bind password = '' for the INSERT | |
| 424 | + | query = query.bind(""); | |
| 425 | + | ||
| 426 | + | query | |
| 427 | + | .execute(&mut *conn) | |
| 428 | + | .await | |
| 429 | + | .map_err(CoreError::database)?; | |
| 430 | + | ||
| 431 | + | Ok(()) | |
| 432 | + | } | |
| 433 | + | ||
| 434 | + | /// Bind a JSON value to a sqlx query. | |
| 435 | + | fn bind_json_value<'q>( | |
| 436 | + | query: sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>>, | |
| 437 | + | val: &'q serde_json::Value, | |
| 438 | + | ) -> sqlx::query::Query<'q, sqlx::Sqlite, sqlx::sqlite::SqliteArguments<'q>> { | |
| 439 | + | match val { | |
| 440 | + | serde_json::Value::String(s) => query.bind(s.as_str()), | |
| 441 | + | serde_json::Value::Number(n) => { | |
| 442 | + | if let Some(i) = n.as_i64() { | |
| 443 | + | query.bind(i) | |
| 444 | + | } else if let Some(f) = n.as_f64() { | |
| 445 | + | query.bind(f) | |
| 446 | + | } else { | |
| 447 | + | query.bind(None::<String>) | |
| 448 | + | } | |
| 449 | + | } | |
| 450 | + | serde_json::Value::Bool(b) => query.bind(if *b { 1i32 } else { 0i32 }), | |
| 451 | + | serde_json::Value::Null => query.bind(None::<String>), | |
| 452 | + | _ => { | |
| 453 | + | // Arrays/objects: serialize as JSON string — need owned String | |
| 454 | + | query.bind(val.to_string()) | |
| 455 | + | } | |
| 456 | + | } | |
| 457 | + | } | |
| 458 | + | ||
| 381 | 459 | /// Apply a DELETE for a remote change. | |
| 382 | - | async fn apply_delete(pool: &SqlitePool, table: &str, row_id: &str) -> Result<(), CoreError> { | |
| 460 | + | async fn apply_delete(conn: &mut SqliteConnection, table: &str, row_id: &str) -> Result<(), CoreError> { | |
| 383 | 461 | // Validate table name is in our whitelist | |
| 384 | 462 | if table_columns(table).is_none() { | |
| 385 | 463 | return Err(CoreError::bad_request(format!("unknown syncable table: {}", table))); | |
| @@ -388,7 +466,7 @@ | |||
| 388 | 466 | let sql = format!("DELETE FROM {} WHERE id = ?", table); | |
| 389 | 467 | sqlx::query(&sql) | |
| 390 | 468 | .bind(row_id) | |
| 391 | - | .execute(pool) | |
| 469 | + | .execute(&mut *conn) | |
| 392 | 470 | .await | |
| 393 | 471 | .map_err(CoreError::database)?; | |
| 394 | 472 | ||
| @@ -667,7 +745,8 @@ | |||
| 667 | 745 | "user_id": user_id, | |
| 668 | 746 | }); | |
| 669 | 747 | ||
| 670 | - | apply_upsert(&pool, "projects", &project_id, &data).await.unwrap(); | |
| 748 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 749 | + | apply_upsert(&mut conn, "projects", &project_id, &data).await.unwrap(); | |
| 671 | 750 | ||
| 672 | 751 | let row: (String,) = sqlx::query_as("SELECT name FROM projects WHERE id = ?") | |
| 673 | 752 | .bind(&project_id) | |
| @@ -693,7 +772,8 @@ | |||
| 693 | 772 | "created_at": now, | |
| 694 | 773 | "user_id": user_id, | |
| 695 | 774 | }); | |
| 696 | - | apply_upsert(&pool, "projects", &project_id, &data1).await.unwrap(); | |
| 775 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 776 | + | apply_upsert(&mut conn, "projects", &project_id, &data1).await.unwrap(); | |
| 697 | 777 | ||
| 698 | 778 | let data2 = json!({ | |
| 699 | 779 | "id": project_id, | |
| @@ -704,7 +784,7 @@ | |||
| 704 | 784 | "created_at": now, | |
| 705 | 785 | "user_id": user_id, | |
| 706 | 786 | }); | |
| 707 | - | apply_upsert(&pool, "projects", &project_id, &data2).await.unwrap(); | |
| 787 | + | apply_upsert(&mut conn, "projects", &project_id, &data2).await.unwrap(); | |
| 708 | 788 | ||
| 709 | 789 | let row: (String, String) = | |
| 710 | 790 | sqlx::query_as("SELECT name, status FROM projects WHERE id = ?") | |
| @@ -720,7 +800,8 @@ | |||
| 720 | 800 | async fn apply_upsert_rejects_unknown_table() { | |
| 721 | 801 | let pool = setup_test_db().await; | |
| 722 | 802 | let data = json!({"id": "abc"}); | |
| 723 | - | let result = apply_upsert(&pool, "nonexistent", "abc", &data).await; | |
| 803 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 804 | + | let result = apply_upsert(&mut conn, "nonexistent", "abc", &data).await; | |
| 724 | 805 | assert!(result.is_err()); | |
| 725 | 806 | let err_msg = result.unwrap_err().to_string(); | |
| 726 | 807 | assert!(err_msg.contains("unknown syncable table")); | |
| @@ -733,6 +814,8 @@ | |||
| 733 | 814 | let project_id = Uuid::new_v4().to_string(); | |
| 734 | 815 | let now = now_sql(); | |
| 735 | 816 | ||
| 817 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 818 | + | ||
| 736 | 819 | // Insert a project to be a FK parent for the task | |
| 737 | 820 | let data_project = json!({ | |
| 738 | 821 | "id": project_id, | |
| @@ -743,7 +826,7 @@ | |||
| 743 | 826 | "created_at": now, | |
| 744 | 827 | "user_id": user_id, | |
| 745 | 828 | }); | |
| 746 | - | apply_upsert(&pool, "projects", &project_id, &data_project).await.unwrap(); | |
| 829 | + | apply_upsert(&mut conn, "projects", &project_id, &data_project).await.unwrap(); | |
| 747 | 830 | ||
| 748 | 831 | let task_id = Uuid::new_v4().to_string(); | |
| 749 | 832 | let data = json!({ | |
| @@ -772,7 +855,7 @@ | |||
| 772 | 855 | "milestone_id": null, | |
| 773 | 856 | }); | |
| 774 | 857 | ||
| 775 | - | apply_upsert(&pool, "tasks", &task_id, &data).await.unwrap(); | |
| 858 | + | apply_upsert(&mut conn, "tasks", &task_id, &data).await.unwrap(); | |
| 776 | 859 | ||
| 777 | 860 | let row: (String,) = sqlx::query_as("SELECT description FROM tasks WHERE id = ?") | |
| 778 | 861 | .bind(&task_id) | |
| @@ -791,6 +874,8 @@ | |||
| 791 | 874 | let project_id = Uuid::new_v4().to_string(); | |
| 792 | 875 | let now = now_sql(); | |
| 793 | 876 | ||
| 877 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 878 | + | ||
| 794 | 879 | let data = json!({ | |
| 795 | 880 | "id": project_id, | |
| 796 | 881 | "name": "To Delete", | |
| @@ -800,9 +885,9 @@ | |||
| 800 | 885 | "created_at": now, | |
| 801 | 886 | "user_id": user_id, | |
| 802 | 887 | }); | |
| 803 | - | apply_upsert(&pool, "projects", &project_id, &data).await.unwrap(); | |
| 888 | + | apply_upsert(&mut conn, "projects", &project_id, &data).await.unwrap(); | |
| 804 | 889 | ||
| 805 | - | apply_delete(&pool, "projects", &project_id).await.unwrap(); | |
| 890 | + | apply_delete(&mut conn, "projects", &project_id).await.unwrap(); | |
| 806 | 891 | ||
| 807 | 892 | let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM projects WHERE id = ?") | |
| 808 | 893 | .bind(&project_id) | |
| @@ -815,7 +900,8 @@ | |||
| 815 | 900 | #[tokio::test] | |
| 816 | 901 | async fn apply_delete_rejects_unknown_table() { | |
| 817 | 902 | let pool = setup_test_db().await; | |
| 818 | - | let result = apply_delete(&pool, "not_a_table", "abc").await; | |
| 903 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 904 | + | let result = apply_delete(&mut conn, "not_a_table", "abc").await; | |
| 819 | 905 | assert!(result.is_err()); | |
| 820 | 906 | let err_msg = result.unwrap_err().to_string(); | |
| 821 | 907 | assert!(err_msg.contains("unknown syncable table")); | |
| @@ -824,8 +910,9 @@ | |||
| 824 | 910 | #[tokio::test] | |
| 825 | 911 | async fn apply_delete_is_idempotent() { | |
| 826 | 912 | let pool = setup_test_db().await; | |
| 913 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 827 | 914 | // Deleting a non-existent row should succeed silently. | |
| 828 | - | let result = apply_delete(&pool, "projects", "nonexistent-id").await; | |
| 915 | + | let result = apply_delete(&mut conn, "projects", "nonexistent-id").await; | |
| 829 | 916 | assert!(result.is_ok()); | |
| 830 | 917 | } | |
| 831 | 918 | ||
| @@ -879,7 +966,8 @@ | |||
| 879 | 966 | ||
| 880 | 967 | // Should succeed despite task coming before project in the input — | |
| 881 | 968 | // apply_changes_inner iterates UPSERT_ORDER so projects is applied first. | |
| 882 | - | apply_changes_inner(&pool, changes).await.unwrap(); | |
| 969 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 970 | + | apply_changes_inner(&mut conn, changes).await.unwrap(); | |
| 883 | 971 | ||
| 884 | 972 | let project: (String,) = sqlx::query_as("SELECT name FROM projects WHERE id = ?") | |
| 885 | 973 | .bind(&project_id) | |
| @@ -905,14 +993,16 @@ | |||
| 905 | 993 | let subtask_id = Uuid::new_v4().to_string(); | |
| 906 | 994 | let now = now_sql(); | |
| 907 | 995 | ||
| 996 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 997 | + | ||
| 908 | 998 | // Insert parent -> child -> grandchild | |
| 909 | - | apply_upsert(&pool, "projects", &project_id, &json!({ | |
| 999 | + | apply_upsert(&mut conn, "projects", &project_id, &json!({ | |
| 910 | 1000 | "id": project_id, "name": "P", "description": "", | |
| 911 | 1001 | "project_type": "Job", "status": "Active", | |
| 912 | 1002 | "created_at": now, "user_id": user_id, | |
| 913 | 1003 | })).await.unwrap(); | |
| 914 | 1004 | ||
| 915 | - | apply_upsert(&pool, "tasks", &task_id, &json!({ | |
| 1005 | + | apply_upsert(&mut conn, "tasks", &task_id, &json!({ | |
| 916 | 1006 | "id": task_id, "project_id": project_id, "description": "T", | |
| 917 | 1007 | "status": "Pending", "priority": "Low", "due": null, | |
| 918 | 1008 | "tags": null, "urgency": 10, "recurrence": "None", | |
| @@ -925,7 +1015,7 @@ | |||
| 925 | 1015 | "contact_id": null, "milestone_id": null, | |
| 926 | 1016 | })).await.unwrap(); | |
| 927 | 1017 | ||
| 928 | - | apply_upsert(&pool, "subtasks", &subtask_id, &json!({ | |
| 1018 | + | apply_upsert(&mut conn, "subtasks", &subtask_id, &json!({ | |
| 929 | 1019 | "id": subtask_id, "task_id": task_id, "text": "Sub", | |
| 930 | 1020 | "is_completed": false, "position": 0, | |
| 931 | 1021 | "created_at": now, "linked_task_id": null, | |
| @@ -939,7 +1029,7 @@ | |||
| 939 | 1029 | change("subtasks", ChangeOp::Delete, &subtask_id, None), | |
| 940 | 1030 | ]; | |
| 941 | 1031 | ||
| 942 | - | apply_changes_inner(&pool, changes).await.unwrap(); | |
| 1032 | + | apply_changes_inner(&mut conn, changes).await.unwrap(); | |
| 943 | 1033 | ||
| 944 | 1034 | let count: (i64,) = | |
| 945 | 1035 | sqlx::query_as("SELECT COUNT(*) FROM projects WHERE id = ?") | |
| @@ -964,9 +1054,11 @@ | |||
| 964 | 1054 | let user_id = create_test_user(&pool).await; | |
| 965 | 1055 | let now = now_sql(); | |
| 966 | 1056 | ||
| 1057 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 1058 | + | ||
| 967 | 1059 | // Create a project to delete later | |
| 968 | 1060 | let delete_id = Uuid::new_v4().to_string(); | |
| 969 | - | apply_upsert(&pool, "projects", &delete_id, &json!({ | |
| 1061 | + | apply_upsert(&mut conn, "projects", &delete_id, &json!({ | |
| 970 | 1062 | "id": delete_id, "name": "Will Delete", "description": "", | |
| 971 | 1063 | "project_type": "Job", "status": "Active", | |
| 972 | 1064 | "created_at": now, "user_id": user_id, | |
| @@ -983,7 +1075,7 @@ | |||
| 983 | 1075 | }))), | |
| 984 | 1076 | ]; | |
| 985 | 1077 | ||
| 986 | - | apply_changes_inner(&pool, changes).await.unwrap(); | |
| 1078 | + | apply_changes_inner(&mut conn, changes).await.unwrap(); | |
| 987 | 1079 | ||
| 988 | 1080 | // The old project was deleted | |
| 989 | 1081 | let count: (i64,) = | |
| @@ -1009,7 +1101,8 @@ | |||
| 1009 | 1101 | ||
| 1010 | 1102 | // INSERT with no data should be skipped (data is None) | |
| 1011 | 1103 | let changes = vec![change("projects", ChangeOp::Insert, "abc", None)]; | |
| 1012 | - | let result = apply_changes_inner(&pool, changes).await; | |
| 1104 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 1105 | + | let result = apply_changes_inner(&mut conn, changes).await; | |
| 1013 | 1106 | assert!(result.is_ok()); | |
| 1014 | 1107 | ||
| 1015 | 1108 | let count: (i64,) = | |
| @@ -1084,29 +1177,9 @@ | |||
| 1084 | 1177 | async fn apply_remote_changes_clears_flag_on_error() { | |
| 1085 | 1178 | let pool = setup_test_db().await; | |
| 1086 | 1179 | ||
| 1087 | - | // Force an error by directly calling apply_upsert on an unknown table, | |
| 1088 | - | // which demonstrates the error path. For apply_remote_changes itself, | |
| 1089 | - | // tables not in UPSERT_ORDER are silently skipped (not errors). | |
| 1090 | - | // Instead, test with a task whose data is malformed enough to cause | |
| 1091 | - | // a DB error — insert a task referencing a non-existent table column. | |
| 1092 | - | // | |
| 1093 | - | // The cleanest approach: manually set the flag, call apply_changes_inner | |
| 1094 | - | // with something that errors, then verify the flag was still cleared | |
| 1095 | - | // by apply_remote_changes. | |
| 1096 | - | ||
| 1097 | - | // We can trigger a real error by having apply_delete called on a table | |
| 1098 | - | // that IS in DELETE_ORDER but whose SQL fails. Actually, apply_delete | |
| 1099 | - | // validates via table_columns, so any table in DELETE_ORDER will pass | |
| 1100 | - | // validation and just do "DELETE FROM X WHERE id = ?" which always | |
| 1101 | - | // succeeds (even if no rows match). | |
| 1102 | - | // | |
| 1103 | - | // The most reliable approach: verify the cleanup pattern by checking | |
| 1104 | - | // the flag is always "0" after apply_remote_changes returns, regardless | |
| 1105 | - | // of content. Test that apply_upsert errors are propagated correctly | |
| 1106 | - | // as a separate unit test. | |
| 1107 | - | ||
| 1108 | 1180 | // Test: apply_upsert on unknown table returns BadRequest | |
| 1109 | - | let result = apply_upsert(&pool, "fake_table", "id", &json!({"id": "x"})).await; | |
| 1181 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 1182 | + | let result = apply_upsert(&mut conn, "fake_table", "id", &json!({"id": "x"})).await; | |
| 1110 | 1183 | assert!(result.is_err()); | |
| 1111 | 1184 | match result.unwrap_err() { | |
| 1112 | 1185 | CoreError::BadRequest(msg) => assert!(msg.contains("unknown syncable table")), | |
| @@ -1323,8 +1396,10 @@ | |||
| 1323 | 1396 | let email_id = Uuid::new_v4().to_string(); | |
| 1324 | 1397 | let now = now_sql(); | |
| 1325 | 1398 | ||
| 1399 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 1400 | + | ||
| 1326 | 1401 | // Insert contact (parent) | |
| 1327 | - | apply_upsert(&pool, "contacts", &contact_id, &json!({ | |
| 1402 | + | apply_upsert(&mut conn, "contacts", &contact_id, &json!({ | |
| 1328 | 1403 | "id": contact_id, "user_id": user_id, | |
| 1329 | 1404 | "display_name": "Test Contact", "nickname": null, | |
| 1330 | 1405 | "company": null, "title": null, "notes": null, | |
| @@ -1333,7 +1408,7 @@ | |||
| 1333 | 1408 | })).await.unwrap(); | |
| 1334 | 1409 | ||
| 1335 | 1410 | // Insert contact_email (child) | |
| 1336 | - | apply_upsert(&pool, "contact_emails", &email_id, &json!({ | |
| 1411 | + | apply_upsert(&mut conn, "contact_emails", &email_id, &json!({ | |
| 1337 | 1412 | "id": email_id, "contact_id": contact_id, | |
| 1338 | 1413 | "address": "test@example.com", "label": "work", | |
| 1339 | 1414 | "is_primary": true, | |
| @@ -1357,7 +1432,9 @@ | |||
| 1357 | 1432 | let email_id = Uuid::new_v4().to_string(); | |
| 1358 | 1433 | let now = now_sql(); | |
| 1359 | 1434 | ||
| 1360 | - | apply_upsert(&pool, "contacts", &contact_id, &json!({ | |
| 1435 | + | let mut conn = pool.acquire().await.unwrap(); | |
| 1436 | + | ||
| 1437 | + | apply_upsert(&mut conn, "contacts", &contact_id, &json!({ | |
| 1361 | 1438 | "id": contact_id, "user_id": user_id, | |
| 1362 | 1439 | "display_name": "Bool Test", "nickname": null, | |
| 1363 | 1440 | "company": null, "title": null, "notes": null, | |
| @@ -1366,7 +1443,7 @@ | |||
| 1366 | 1443 | })).await.unwrap(); | |
| 1367 | 1444 | ||
| 1368 | 1445 | // is_primary as a JSON boolean — should be stored as integer | |
| 1369 | - | apply_upsert(&pool, "contact_emails", &email_id, &json!({ | |
| 1446 | + | apply_upsert(&mut conn, "contact_emails", &email_id, &json!({ | |
| 1370 | 1447 | "id": email_id, "contact_id": contact_id, | |
| 1371 | 1448 | "address": "bool@test.com", "label": "home", | |
| 1372 | 1449 | "is_primary": true, | |
| @@ -1419,8 +1496,12 @@ | |||
| 1419 | 1496 | .unwrap(); | |
| 1420 | 1497 | let parsed: serde_json::Value = serde_json::from_str(&data.0).unwrap(); | |
| 1421 | 1498 | let obj = parsed.as_object().unwrap(); | |
| 1422 | - | assert_eq!(obj.len(), 20, "trigger data should have 20 keys"); | |
| 1423 | - | assert!(!obj.contains_key("last_sync_at"), "last_sync_at should not be synced"); | |
| 1499 | + | assert_eq!(obj.len(), 16, "trigger data should have 16 keys (config only)"); | |
| 1500 | + | assert!(!obj.contains_key("password"), "password should not be in trigger data"); | |
| 1501 | + | assert!(!obj.contains_key("oauth2_access_token"), "oauth2_access_token should not be in trigger data"); | |
| 1502 | + | assert!(!obj.contains_key("oauth2_refresh_token"), "oauth2_refresh_token should not be in trigger data"); | |
| 1503 | + | assert!(!obj.contains_key("oauth2_token_expires_at"), "oauth2_token_expires_at should not be in trigger data"); | |
| 1504 | + | assert!(!obj.contains_key("last_sync_at"), "last_sync_at should not be in trigger data"); | |
| 1424 | 1505 | assert_eq!(parsed["account_name"], "Work"); | |
| 1425 | 1506 | } | |
| 1426 | 1507 | ||
| @@ -1465,6 +1546,7 @@ | |||
| 1465 | 1546 | ||
| 1466 | 1547 | let parsed: serde_json::Value = serde_json::from_str(&row.1).unwrap(); | |
| 1467 | 1548 | assert_eq!(parsed["account_name"], "Personal"); | |
| 1549 | + | assert!(!parsed.as_object().unwrap().contains_key("password"), "update trigger should not include password"); | |
| 1468 | 1550 | } | |
| 1469 | 1551 | ||
| 1470 | 1552 | #[tokio::test] | |
| @@ -1508,9 +1590,224 @@ | |||
| 1508 | 1590 | } | |
| 1509 | 1591 |
Lines truncated
| @@ -886,9 +886,9 @@ | |||
| 886 | 886 | #[test] | |
| 887 | 887 | fn json_to_dynamic_primitives() { | |
| 888 | 888 | assert_eq!(json_to_dynamic(serde_json::Value::Null).type_name(), "()"); | |
| 889 | - | assert_eq!(json_to_dynamic(serde_json::Value::Bool(true)).as_bool().unwrap(), true); | |
| 889 | + | assert!(json_to_dynamic(serde_json::Value::Bool(true)).as_bool().unwrap()); | |
| 890 | 890 | assert_eq!(json_to_dynamic(serde_json::json!(42)).as_int().unwrap(), 42); | |
| 891 | - | assert_eq!(json_to_dynamic(serde_json::json!(3.14)).as_float().unwrap(), 3.14); | |
| 891 | + | assert_eq!(json_to_dynamic(serde_json::json!(2.72)).as_float().unwrap(), 2.72); | |
| 892 | 892 | assert_eq!( | |
| 893 | 893 | json_to_dynamic(serde_json::json!("hello")).into_string().unwrap(), | |
| 894 | 894 | "hello" | |
| @@ -903,7 +903,7 @@ | |||
| 903 | 903 | assert_eq!(arr.len(), 3); | |
| 904 | 904 | assert_eq!(arr[0].as_int().unwrap(), 1); | |
| 905 | 905 | assert_eq!(arr[1].clone().into_string().unwrap(), "two"); | |
| 906 | - | assert_eq!(arr[2].as_bool().unwrap(), true); | |
| 906 | + | assert!(arr[2].as_bool().unwrap()); | |
| 907 | 907 | } | |
| 908 | 908 | ||
| 909 | 909 | #[test] |
| @@ -820,9 +820,9 @@ | |||
| 820 | 820 | ||
| 821 | 821 | // ImportOptions derives Default, so has_header is false (the | |
| 822 | 822 | // serde default_true function only applies during deserialization). | |
| 823 | - | assert_eq!(map.get("has_header").unwrap().as_bool().unwrap(), false); | |
| 824 | - | assert!(map.get("delimiter").is_none()); | |
| 825 | - | assert!(map.get("date_format").is_none()); | |
| 823 | + | assert!(!map.get("has_header").unwrap().as_bool().unwrap()); | |
| 824 | + | assert!(!map.contains_key("delimiter")); | |
| 825 | + | assert!(!map.contains_key("date_format")); | |
| 826 | 826 | } | |
| 827 | 827 | ||
| 828 | 828 | #[test] | |
| @@ -840,7 +840,7 @@ | |||
| 840 | 840 | ||
| 841 | 841 | let map = options_to_rhai_map(&options); | |
| 842 | 842 | ||
| 843 | - | assert_eq!(map.get("has_header").unwrap().as_bool().unwrap(), false); | |
| 843 | + | assert!(!map.get("has_header").unwrap().as_bool().unwrap()); | |
| 844 | 844 | assert_eq!( | |
| 845 | 845 | map.get("delimiter").unwrap().clone().into_string().unwrap(), | |
| 846 | 846 | "\t" |