feat: Add Web Push notifications and multiple UX improvements
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:
Paul Nothaft
2026-01-19 20:53:13 +01:00
parent 9bfc9e2f96
commit f91978c078
15 changed files with 1158 additions and 126 deletions
+121
View File
@@ -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;
}