diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..7473e94
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/JobsMedical.Web/JobsMedical.Web.csproj b/src/JobsMedical.Web/JobsMedical.Web.csproj
index 614e444..efa5057 100644
--- a/src/JobsMedical.Web/JobsMedical.Web.csproj
+++ b/src/JobsMedical.Web/JobsMedical.Web.csproj
@@ -12,6 +12,7 @@
all
+
diff --git a/src/JobsMedical.Web/Program.cs b/src/JobsMedical.Web/Program.cs
index aff6886..d6d10a6 100644
--- a/src/JobsMedical.Web/Program.cs
+++ b/src/JobsMedical.Web/Program.cs
@@ -24,6 +24,7 @@ builder.Services.AddScoped();
builder.Services.AddSingleton();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
// Listing parser: heuristic now; swap for an LLM-backed IListingParser later.
builder.Services.AddSingleton();
diff --git a/src/JobsMedical.Web/Services/NotificationService.cs b/src/JobsMedical.Web/Services/NotificationService.cs
index e5477df..2cad7ee 100644
--- a/src/JobsMedical.Web/Services/NotificationService.cs
+++ b/src/JobsMedical.Web/Services/NotificationService.cs
@@ -12,11 +12,13 @@ namespace JobsMedical.Web.Services;
public class NotificationService
{
private readonly AppDbContext _db;
+ private readonly PushNotifier _push;
private readonly ILogger _log;
- public NotificationService(AppDbContext db, ILogger log)
+ public NotificationService(AppDbContext db, PushNotifier push, ILogger log)
{
_db = db;
+ _push = push;
_log = log;
}
@@ -74,5 +76,9 @@ public class NotificationService
_db.Notifications.Add(new Notification { UserId = uid, Title = title, Body = body, Url = url });
await _db.SaveChangesAsync();
_log.LogInformation("Notified {Count} users: {Title}", userIds.Count, title);
+
+ // Also push to the lock screen for users who subscribed (best-effort).
+ try { await _push.PushToUsersAsync(userIds, title, body, url); }
+ catch (Exception ex) { _log.LogWarning(ex, "Web push fan-out failed"); }
}
}
diff --git a/src/JobsMedical.Web/Services/PushNotifier.cs b/src/JobsMedical.Web/Services/PushNotifier.cs
new file mode 100644
index 0000000..2ec9dee
--- /dev/null
+++ b/src/JobsMedical.Web/Services/PushNotifier.cs
@@ -0,0 +1,69 @@
+using System.Text.Json;
+using JobsMedical.Web.Data;
+using JobsMedical.Web.Models;
+using JobsMedical.Web.Services.Scraping;
+using Microsoft.EntityFrameworkCore;
+using WebPush;
+
+namespace JobsMedical.Web.Services;
+
+///
+/// Sends Web Push notifications (lock-screen) to users' subscribed browsers via VAPID. Rides the
+/// same in-app notification records — when users are notified, their subscriptions also get a push.
+/// Config (enable + VAPID keys) comes from AppSetting; dead subscriptions (404/410) are pruned.
+/// ⚠️ Chrome push goes through FCM (may be filtered in Iran); the Bazaar TWA can use native push.
+///
+public class PushNotifier
+{
+ private readonly AppDbContext _db;
+ private readonly SettingsService _settings;
+ private readonly ILogger _log;
+
+ public PushNotifier(AppDbContext db, SettingsService settings, ILogger log)
+ {
+ _db = db;
+ _settings = settings;
+ _log = log;
+ }
+
+ public async Task PushToUsersAsync(IReadOnlyCollection userIds, string title, string? body, string url)
+ {
+ if (userIds.Count == 0) return;
+ var s = await _settings.GetAsync();
+ if (!s.PushEnabled || string.IsNullOrWhiteSpace(s.VapidPublicKey) || string.IsNullOrWhiteSpace(s.VapidPrivateKey))
+ return;
+
+ var subs = await (from sub in _db.WebPushSubscriptions
+ join v in _db.Visitors on sub.VisitorId equals v.Id
+ where v.UserId != null && userIds.Contains(v.UserId.Value)
+ select sub).ToListAsync();
+ if (subs.Count == 0) return;
+
+ var client = new WebPushClient();
+ var vapid = new VapidDetails(s.VapidSubject ?? "mailto:admin@hamkadr.ir", s.VapidPublicKey, s.VapidPrivateKey);
+ var payload = JsonSerializer.Serialize(new { title, body, url });
+
+ var dead = new List();
+ foreach (var sub in subs)
+ {
+ try
+ {
+ await client.SendNotificationAsync(
+ new WebPush.PushSubscription(sub.Endpoint, sub.P256dh, sub.Auth), payload, vapid);
+ }
+ catch (WebPushException ex) when ((int)ex.StatusCode is 404 or 410)
+ {
+ dead.Add(sub); // subscription expired/gone
+ }
+ catch (Exception ex)
+ {
+ _log.LogWarning(ex, "Web push send failed for one subscription");
+ }
+ }
+ if (dead.Count > 0)
+ {
+ _db.WebPushSubscriptions.RemoveRange(dead);
+ await _db.SaveChangesAsync();
+ }
+ }
+}