Phase 3-4: Goals AI planner, Team tasks, Push notifications, i18n (CZ/HE/RU/UA)

- Goals CRUD API + AI study plan generator + progress reports
- Goals frontend page with progress bars
- Team tasks: assign, transfer, collaborate endpoints
- Push notifications: web-push, VAPID, subscribe/send
- i18n: 4 languages (cs, he, ru, ua) translation files
- notifications.js + goals.js routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude CLI Agent
2026-03-29 13:12:19 +00:00
parent eac9e72404
commit fea4d38ce8
19 changed files with 1176 additions and 112 deletions

View File

@@ -0,0 +1,77 @@
// Task Team — Push Notifications — 2026-03-29
const webpush = require("web-push");
async function notificationRoutes(app) {
// Generate VAPID keys if not exist
const vapidKeys = {
publicKey: process.env.VAPID_PUBLIC_KEY || "",
privateKey: process.env.VAPID_PRIVATE_KEY || ""
};
if (!vapidKeys.publicKey) {
const keys = webpush.generateVAPIDKeys();
vapidKeys.publicKey = keys.publicKey;
vapidKeys.privateKey = keys.privateKey;
console.log("VAPID keys generated. Add to env:", JSON.stringify(keys));
}
webpush.setVapidDetails("mailto:admin@hasdo.info", vapidKeys.publicKey, vapidKeys.privateKey);
// Create subscriptions table if not exists
await app.db.query(`
CREATE TABLE IF NOT EXISTS push_subscriptions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
subscription JSONB NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
)
`);
// Get VAPID public key
app.get("/notifications/vapid-key", async () => {
return { data: { publicKey: vapidKeys.publicKey } };
});
// Subscribe
app.post("/notifications/subscribe", async (req) => {
const { subscription, user_id } = req.body;
await app.db.query(
"INSERT INTO push_subscriptions (user_id, subscription) VALUES ($1, $2)",
[user_id, JSON.stringify(subscription)]
);
return { status: "subscribed" };
});
// Send notification (internal use)
app.post("/notifications/send", async (req) => {
const { user_id, title, body, url } = req.body;
const { rows } = await app.db.query(
"SELECT subscription FROM push_subscriptions WHERE user_id = $1", [user_id]
);
let sent = 0;
for (const row of rows) {
try {
await webpush.sendNotification(
JSON.parse(row.subscription),
JSON.stringify({ title, body, url: url || "/tasks" })
);
sent++;
} catch (e) {
if (e.statusCode === 410) {
await app.db.query("DELETE FROM push_subscriptions WHERE subscription = $1", [row.subscription]);
}
}
}
return { status: "sent", count: sent };
});
// Unsubscribe
app.delete("/notifications/unsubscribe", async (req) => {
const { user_id } = req.body;
await app.db.query("DELETE FROM push_subscriptions WHERE user_id = $1", [user_id]);
return { status: "unsubscribed" };
});
}
module.exports = notificationRoutes;