PWA: installable app (web/win/android/ios) + download/help page + push notifications
CI/CD / CI · dotnet build (push) Successful in 40s
CI/CD / Deploy · hamkadr (push) Successful in 55s

- manifest.webmanifest + service worker (offline shell + push + notificationclick) + PNG icons (192/512/apple) + iOS meta + SW registration → installable everywhere
- /Download page: per-OS install help (web/windows/android/ios), install button (beforeinstallprompt), 'enable notifications' flow, usage guide, Bazaar/TWA note; nav + footer links
- Web Push foundation: WebPushSubscription entity + /push/subscribe (stores), VAPID + push settings in /Admin/Settings, on-device local notification; server broadcast documented (WebPush via Nexus)
- docs/PWA-TWA.md: VAPID keygen, server-push wiring, Bubblewrap→Cafe Bazaar + assetlinks steps
- Verified: manifest/sw/icons served, download page, subscribe stores (200), layout wired

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 11:23:13 +03:30
parent 9a92da42e6
commit a02eb6a985
17 changed files with 1417 additions and 1 deletions
@@ -93,6 +93,9 @@ namespace JobsMedical.Web.Migrations
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<bool>("PushEnabled")
.HasColumnType("boolean");
b.Property<string>("SmsApiKey")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
@@ -118,6 +121,18 @@ namespace JobsMedical.Web.Migrations
b.Property<DateTime>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("VapidPrivateKey")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("VapidPublicKey")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("VapidSubject")
.HasMaxLength(120)
.HasColumnType("character varying(120)");
b.HasKey("Id");
b.ToTable("AppSettings");
@@ -668,6 +683,44 @@ namespace JobsMedical.Web.Migrations
b.ToTable("Visitors");
});
modelBuilder.Entity("JobsMedical.Web.Models.WebPushSubscription", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Auth")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Endpoint")
.IsRequired()
.HasMaxLength(600)
.HasColumnType("character varying(600)");
b.Property<string>("P256dh")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("VisitorId")
.HasMaxLength(36)
.HasColumnType("character varying(36)");
b.HasKey("Id");
b.HasIndex("Endpoint")
.IsUnique();
b.ToTable("WebPushSubscriptions");
});
modelBuilder.Entity("JobsMedical.Web.Models.Application", b =>
{
b.HasOne("JobsMedical.Web.Models.User", "Doctor")