feat: push upozornění na doručení objednávky Bolt Food
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m48s
CI / Build and push Docker image (push) Successful in 53s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m48s
CI / Build and push Docker image (push) Successful in 53s
CI / Notify (push) Successful in 2s
Nové zaškrtávátko v Nastavení → Notifikace pod výběrem času připomínky. Když skupinová objednávka sledovaná přes Bolt Food přejde do stavu delivered/finished, dostanou push členové skupiny, kteří si to zapnuli. Zakladatel skupiny notifikaci nedostává — ten je informován aplikací Bolt. Push subscription byla dosud svázaná s časem připomínky (subscribe endpoint bez reminderTime vracel 400), takže by nový přepínač samostatně nefungoval. Čas připomínky je nově v registru volitelný a scheduler připomínek záznamy bez času přeskakuje. Service worker nemá natvrdo tag a akci "Mám vlastní/neobědvám" — obojí se řídí payloadem, aby je doručovací notifikace nedědila.
This commit is contained in:
1 parent
377a350211
commit
065ccaf38a
12 files changed
+440
-41
No files matched your search
+61
-12
@@ -13,10 +13,21 @@ const lease = createLeaderLease('luncher:reminder:leader');
|
||||
const POD_ID = process.env.POD_ID ?? `local-${process.pid}`;
|
||||
|
||||
interface RegistryEntry {
|
||||
time: string;
|
||||
/** Čas připomínky výběru oběda (HH:MM). Nevyplněno = uživatel odebírá jen ostatní push notifikace. */
|
||||
time?: string;
|
||||
subscription: webpush.PushSubscription;
|
||||
}
|
||||
|
||||
/** Data odesílaná do service workeru. */
|
||||
export interface PushPayload {
|
||||
title: string;
|
||||
body: string;
|
||||
/** Seskupovací tag notifikace (výchozí v service workeru je připomínka oběda). */
|
||||
tag?: string;
|
||||
/** Cesta, která se otevře po kliknutí na notifikaci. */
|
||||
url?: string;
|
||||
}
|
||||
|
||||
type Registry = Record<string, RegistryEntry>;
|
||||
|
||||
/** Mapa login → timestamp (ms) posledního odeslání připomínky. */
|
||||
@@ -55,14 +66,18 @@ export function stopReminderScheduler(): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Přidá nebo aktualizuje push subscription pro uživatele. */
|
||||
export async function subscribePush(login: string, subscription: webpush.PushSubscription, reminderTime: string): Promise<void> {
|
||||
/**
|
||||
* Přidá nebo aktualizuje push subscription pro uživatele.
|
||||
* `reminderTime` je volitelný — bez něj uživatel dostává jen ostatní push notifikace
|
||||
* (např. doručení objednávky Bolt Food), ale ne připomínku výběru oběda.
|
||||
*/
|
||||
export async function subscribePush(login: string, subscription: webpush.PushSubscription, reminderTime?: string): Promise<void> {
|
||||
await storage.updateData<Registry>(REGISTRY_KEY, (current) => {
|
||||
const registry = current ?? {};
|
||||
registry[login] = { time: reminderTime, subscription };
|
||||
return registry;
|
||||
});
|
||||
console.log(`Push reminder: uživatel ${login} přihlášen k připomínkám v ${reminderTime}`);
|
||||
console.log(`Push reminder: uživatel ${login} přihlášen k push notifikacím${reminderTime ? `, připomínka v ${reminderTime}` : ' (bez připomínky oběda)'}`);
|
||||
}
|
||||
|
||||
/** Odebere push subscription pro uživatele. */
|
||||
@@ -94,6 +109,45 @@ export function verifyQuickChoiceToken(login: string, token: string): boolean {
|
||||
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(token, 'hex'));
|
||||
}
|
||||
|
||||
/** Odebere z registru subscriptions, které push služba označila za neplatné (404/410). */
|
||||
async function pruneExpiredSubscriptions(expiredLogins: string[]): Promise<void> {
|
||||
if (expiredLogins.length === 0) return;
|
||||
await storage.updateData<Registry>(REGISTRY_KEY, (current) => {
|
||||
const registry = current ?? {};
|
||||
for (const login of expiredLogins) delete registry[login];
|
||||
return registry;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Odešle push notifikaci daným uživatelům. Loginy bez registrované subscription
|
||||
* se tiše přeskočí, expirované subscriptions se z registru odeberou.
|
||||
*/
|
||||
export async function sendPushToLogins(logins: string[], payload: PushPayload): Promise<void> {
|
||||
if (logins.length === 0) return;
|
||||
|
||||
const registry = await storage.getData<Registry>(REGISTRY_KEY) ?? {};
|
||||
const expiredLogins: string[] = [];
|
||||
|
||||
for (const login of logins) {
|
||||
const entry = registry[login];
|
||||
if (!entry) continue;
|
||||
try {
|
||||
await webpush.sendNotification(entry.subscription, JSON.stringify(payload));
|
||||
console.log(`Push: odeslána notifikace "${payload.title}" uživateli ${login}`);
|
||||
} catch (error: any) {
|
||||
if (error.statusCode === 410 || error.statusCode === 404) {
|
||||
console.log(`Push: subscription uživatele ${login} expirovala, odebírám`);
|
||||
expiredLogins.push(login);
|
||||
} else {
|
||||
console.error(`Push: chyba při odesílání notifikace uživateli ${login}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await pruneExpiredSubscriptions(expiredLogins);
|
||||
}
|
||||
|
||||
/** Zkontroluje a odešle připomínky uživatelům, kteří si nezvolili oběd. */
|
||||
async function checkAndSendReminders(): Promise<void> {
|
||||
if (getIsWeekend(getToday())) return;
|
||||
@@ -119,7 +173,8 @@ async function checkAndSendReminders(): Promise<void> {
|
||||
const expiredLogins: string[] = [];
|
||||
|
||||
for (const [login, entry] of entries) {
|
||||
if (currentTime < entry.time) continue;
|
||||
// Uživatelé bez nastaveného času připomínky odebírají jen ostatní push notifikace
|
||||
if (!entry.time || currentTime < entry.time) continue;
|
||||
|
||||
const last = lastReminded.get(login) ?? 0;
|
||||
if (Date.now() - last < REMINDER_COOLDOWN_MS) continue;
|
||||
@@ -148,13 +203,7 @@ async function checkAndSendReminders(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
if (expiredLogins.length > 0) {
|
||||
await storage.updateData<Registry>(REGISTRY_KEY, (current) => {
|
||||
const r = current ?? {};
|
||||
for (const login of expiredLogins) delete r[login];
|
||||
return r;
|
||||
});
|
||||
}
|
||||
await pruneExpiredSubscriptions(expiredLogins);
|
||||
}
|
||||
|
||||
/** Spustí scheduler pro kontrolu a odesílání připomínek každou minutu. */
|
||||
|
||||
Reference in new issue
Block a user