feat(tickets): Web-Push-Benachrichtigungen (PWA)
- Die Züchterin kann auf der Tickets-Seite Push-Benachrichtigungen aktivieren (🔔-Schalter). Service Worker ist BEWUSST push-only (kein fetch/Caching), damit das Laden der App nie beeinträchtigt wird. - Backend: WebPush-Bibliothek + VAPID; PushNotifier sendet an alle Abos, räumt veraltete (404/410) auf. Endpoints: GET /push/vapid-public-key, POST /push/subscribe|unsubscribe. - Ausgelöst über ein notify-Flag auf PUT /feedback: NUR die KI setzt es (z. B. neue Rückfrage / Ticket gelöst) → keine Selbst-Pushes durch Aktionen der Züchterin. Text wird aus dem Status abgeleitet, Klick öffnet das Ticket (?focus=). - Schalter/Logik blenden sich aus, wenn das Gerät kein Push kann oder der Server keine VAPID-Schlüssel hat (z. B. e2e-Mock). Migration WebPushSubscriptions. VAPID-Schlüssel in appsettings.json (lokale Single-User-App im Heimnetz — bewusste, vertretbare Vereinfachung). Tests: 264 Backend grün (+Push-Endpoints), e2e Tickets Desktop grün, vitest 149. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
74
GerbilManagerWebAPI/Endpoints/PushEndpoints.cs
Normal file
74
GerbilManagerWebAPI/Endpoints/PushEndpoints.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using GerbilManagerWebAPI.Models;
|
||||
using GerbilManagerWebAPI.Push;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GerbilManagerWebAPI.Endpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// WEB-PUSH: PWA-Benachrichtigungen für die Züchterin.
|
||||
/// GET /push/vapid-public-key -> öffentlicher VAPID-Schlüssel (für das Abonnieren im Browser).
|
||||
/// POST /push/subscribe -> Browser-Abo speichern (idempotent über die Endpoint-URL).
|
||||
/// POST /push/unsubscribe -> Abo entfernen.
|
||||
/// Das eigentliche Senden passiert über <see cref="PushNotifier"/> (z. B. wenn die KI eine
|
||||
/// Rückfrage stellt oder ein Ticket löst — gesteuert über das notify-Flag auf PUT /feedback).
|
||||
/// </summary>
|
||||
public static class PushEndpoints
|
||||
{
|
||||
public record PushSubscriptionInput(string Endpoint, string P256dh, string Auth);
|
||||
public record PushUnsubscribeInput(string Endpoint);
|
||||
|
||||
public static IEndpointRouteBuilder MapPushEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/push").WithTags("Push");
|
||||
|
||||
group.MapGet("/vapid-public-key", (PushNotifier push) =>
|
||||
TypedResults.Ok(new { publicKey = push.PublicKey, enabled = push.Enabled }));
|
||||
|
||||
group.MapPost("/subscribe", async Task<Results<Ok, BadRequest<string>>> (
|
||||
PushSubscriptionInput input, ApplicationContext db) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.Endpoint)
|
||||
|| string.IsNullOrWhiteSpace(input.P256dh)
|
||||
|| string.IsNullOrWhiteSpace(input.Auth))
|
||||
return TypedResults.BadRequest("Endpoint, P256dh und Auth sind erforderlich.");
|
||||
|
||||
var existing = await db.WebPushSubscriptions
|
||||
.FirstOrDefaultAsync(s => s.Endpoint == input.Endpoint);
|
||||
if (existing is null)
|
||||
{
|
||||
db.WebPushSubscriptions.Add(new WebPushSubscription
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Endpoint = input.Endpoint,
|
||||
P256dh = input.P256dh,
|
||||
Auth = input.Auth,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.P256dh = input.P256dh;
|
||||
existing.Auth = input.Auth;
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
return TypedResults.Ok();
|
||||
});
|
||||
|
||||
group.MapPost("/unsubscribe", async (PushUnsubscribeInput input, ApplicationContext db) =>
|
||||
{
|
||||
var subs = await db.WebPushSubscriptions
|
||||
.Where(s => s.Endpoint == input.Endpoint)
|
||||
.ToListAsync();
|
||||
if (subs.Count > 0)
|
||||
{
|
||||
db.WebPushSubscriptions.RemoveRange(subs);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
return TypedResults.NoContent();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user