fix: address Greptile review feedback master
Four issues flagged by Greptile on #1211: 1. Expired subscriptions never pruned from blast path. The ExpiredSubscriptionError type existed but no caller unwrapped it to delete dead rows. WebPushNotifier.Blast now collects expired tokens into a BlastsError; ExpiredTokens(err) walks the MultiNotifier → errors.Join → BlastsError tree to extract them. Both blast call sites (queue_processor, sync.go) now prune dead subscriptions after each blast so they don't accumulate and burn time on every notification. 2. HandleVapidPublicKey manually concatenated JSON. Replaced with json.NewEncoder so serialization is always correct. 3. Notifications settings screen forced a re-render every second to refresh Notification.permission. Replaced with the Permissions API onchange listener, which fires only on actual state transitions. 4. disableWebNotifications cleared notificationToken in a finally block even when the server DELETE failed, leaving the user unable to retry while the server kept pushing. Now only clears after a confirmed successful DELETE; on failure the token stays set so the toggle remains "on" and the user can retry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>