feat: Add Web Push notifications and multiple UX improvements
continuous-integration/drone/push Build is failing
continuous-integration/drone/push Build is failing
- Add PWA manifest and service worker for push notifications
- Implement VAPID key generation and push subscription management
- Add push notification API endpoints (/api/push/*)
- Add push_subscriptions table to database schema
- Update notification settings UI with push support and iOS hints
- Fix translation issue showing "{ task } created" - add missing keys
- Fix mobile sidebar visibility for iOS home screen app
- Change default task filter from "all" to "open" (excludes done tasks)
- Add "Open" filter option to show only todo + inProgress tasks
This commit is contained in:
@@ -8,6 +8,7 @@ import { mcpServer } from "./mcp";
|
||||
import { AiService, DEFAULT_SYSTEM_PROMPT } from "./ai.js";
|
||||
import { RecurrenceService } from "./services/recurrence.js";
|
||||
import { GamificationService } from "./gamification.js";
|
||||
import { getVapidPublicKey, savePushSubscription, removePushSubscription, removeUserSubscriptions, sendPushNotification } from "./push.js";
|
||||
|
||||
const emailService = new EmailService(storage);
|
||||
const aiService = new AiService(storage);
|
||||
@@ -2219,6 +2220,126 @@ ${activeTasksWithLabels.slice(0, 5).map(t => `- [${t.priority}] [${t.label}] ${t
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================
|
||||
// PUSH NOTIFICATION ROUTES
|
||||
// ============================================
|
||||
|
||||
// Get VAPID public key (needed by client to subscribe)
|
||||
app.get("/api/push/vapid-public-key", async (req, res) => {
|
||||
try {
|
||||
const publicKey = await getVapidPublicKey();
|
||||
if (!publicKey) {
|
||||
return res.status(503).json({ error: "Push notifications not configured" });
|
||||
}
|
||||
res.json({ publicKey });
|
||||
} catch (e) {
|
||||
console.error("Get VAPID key failed:", e);
|
||||
res.status(500).json({ error: "Failed to get VAPID key" });
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to push notifications
|
||||
app.post("/api/push/subscribe", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const { subscription } = req.body;
|
||||
if (!subscription || !subscription.endpoint || !subscription.keys) {
|
||||
return res.status(400).json({ error: "Invalid subscription data" });
|
||||
}
|
||||
|
||||
const userAgent = req.headers['user-agent'];
|
||||
const success = await savePushSubscription(userId, subscription, userAgent);
|
||||
|
||||
if (success) {
|
||||
// Send a welcome notification to confirm it works
|
||||
await sendPushNotification(userId, {
|
||||
title: "Notifications Enabled",
|
||||
body: "You'll now receive task reminders even when the app is closed!",
|
||||
tag: "welcome"
|
||||
});
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(500).json({ error: "Failed to save subscription" });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Subscribe failed:", e);
|
||||
res.status(500).json({ error: "Failed to subscribe" });
|
||||
}
|
||||
});
|
||||
|
||||
// Unsubscribe from push notifications
|
||||
app.post("/api/push/unsubscribe", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
try {
|
||||
const { endpoint } = req.body;
|
||||
if (!endpoint) {
|
||||
return res.status(400).json({ error: "Endpoint required" });
|
||||
}
|
||||
|
||||
const success = await removePushSubscription(endpoint);
|
||||
res.json({ success });
|
||||
} catch (e) {
|
||||
console.error("Unsubscribe failed:", e);
|
||||
res.status(500).json({ error: "Failed to unsubscribe" });
|
||||
}
|
||||
});
|
||||
|
||||
// Unsubscribe all devices for current user
|
||||
app.delete("/api/push/subscriptions", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const success = await removeUserSubscriptions(userId);
|
||||
res.json({ success });
|
||||
} catch (e) {
|
||||
console.error("Remove subscriptions failed:", e);
|
||||
res.status(500).json({ error: "Failed to remove subscriptions" });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle subscription changes (from service worker)
|
||||
app.post("/api/push/resubscribe", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const { oldEndpoint, newSubscription } = req.body;
|
||||
|
||||
// Remove old subscription
|
||||
if (oldEndpoint) {
|
||||
await removePushSubscription(oldEndpoint);
|
||||
}
|
||||
|
||||
// Save new subscription
|
||||
if (newSubscription) {
|
||||
const userAgent = req.headers['user-agent'];
|
||||
await savePushSubscription(userId, newSubscription, userAgent);
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
console.error("Resubscribe failed:", e);
|
||||
res.status(500).json({ error: "Failed to resubscribe" });
|
||||
}
|
||||
});
|
||||
|
||||
// Test notification (useful for debugging)
|
||||
app.post("/api/push/test", async (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.sendStatus(401);
|
||||
const userId = (req.user as User).id;
|
||||
try {
|
||||
const result = await sendPushNotification(userId, {
|
||||
title: "Test Notification",
|
||||
body: "Push notifications are working!",
|
||||
tag: "test"
|
||||
});
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
console.error("Test notification failed:", e);
|
||||
res.status(500).json({ error: "Failed to send test notification" });
|
||||
}
|
||||
});
|
||||
|
||||
// End of routes
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user