- 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>
75 lines
3.1 KiB
C#
75 lines
3.1 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|