Add a 'notification channels' card at the top of admin Settings with three master on/off checkboxes: web/in-app (new WebNotificationsEnabled, default true), SMS (existing SmsEnabled), and Web Push (existing PushEnabled). Removed the duplicate enable checkboxes from the SMS and Push sections so each binds once. NotificationService now gates the in-app + live SSE channel on WebNotificationsEnabled; push self-gates on PushEnabled. Migration defaults the new column to true so existing installs keep web notifications on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Web Push is delivered by the browser vendor's push service (Chrome to Google FCM), which is filtered in Iran, so background push is unreliable. Add a Server-Sent Events channel over our own origin that always reaches users while the tab/PWA is open: NotificationHub (in-memory pub/sub), a /notifications/stream SSE endpoint (auth-gated, keep-alive pings, nginx no-buffer), and NotificationService now publishes each saved notification to the hub. Client updates the bell badge instantly, shows a toast, and fires a local OS notification via the service worker when permission is granted (no push server). Web Push stays as best-effort for closed-app reach. Verified end-to-end: login, open stream, broadcast, event delivered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrap the bell label in a span so the conditional parses as Razor code instead of leaking as literal text (Persian word char before the at-sign triggered email detection). Bell is icon-only on desktop, label shows in the mobile dropdown. Add a Settings link to admin actions so the demo-mode page is reachable from the navbar. Tighten desktop nav/action spacing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mirror.soroushasadi.com serves a leaf-only TLS chain (no intermediate).
.NET on Linux does not auto-fetch the intermediate via AIA like Windows
does, so CI/Docker restores fail with NU1301 PartialChain. Switch the
Linux build configs (CI inline config + nuget.docker.config) to the
Liara mirror, which serves a complete chain. Also disable NuGetAudit to
avoid the api.nuget.org (filtered) 100s timeout + NU1900 noise.
Local dev nuget.config keeps Nexus primary (Windows resolves the chain).
Re-add Nexus to the Linux configs once nginx serves fullchain.pem.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- _Layout: wrap nav + actions in .nav-collapse; add CSS-only burger
(checkbox toggle) + always-visible mobile bell next to it
- site.css: desktop layout unchanged; <=860px collapses nav into a
full-width dropdown (column links, stacked actions), animated open
- Login (ورود) + logout render as full-width buttons in the menu
instead of overflowing the header row
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- nuget.config with Soroush Nexus + Liara mirrors (nuget.org filtered); added WebPush 1.0.12
- PushNotifier: VAPID send to a user's subscriptions, prunes dead (404/410); config from AppSetting
- NotificationService fans out a Web Push to matched users' subscribed browsers after creating in-app notifications (best-effort; no-op until admin enables push + sets VAPID)
- Build verified through the mirrors; app boots with PushNotifier wired
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Notification entity + NotificationService: on publish, notify users whose saved prefs match the listing (role/city/+shift type); users with no preference aren't spammed
- Wired into PostShift, PostJob, and Admin Review publish
- 🔔 bell with unread count in the header (@inject) + /Me/Notifications page (mark-all-read on open)
- Reliable in-app delivery (works in Iran without FCM); Web Push can ride the same records later
- Verified: employee pref → employer posts matching shift → employee bell=۱ + 'شیفت جدید: پزشک عمومی'
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- RegisterFacility: '📍 موقعیت فعلی من' (browser geolocation, always available) + Neshan Leaflet map (click/drag marker → fills lat/lng) when a Neshan web key is set; graceful fallback to manual coords without a key
- AppSetting.NeshanMapKey configured in /Admin/Settings (Google Maps is blocked in Iran); migration
- Verified: location button + inputs render always; map + SDK render once the key is saved
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ListingPolicy.JobFreshnessDays=30: public /Jobs and home hide jobs older than the cutoff (shifts already require Date>=today)
- ListingArchiver flips stale Open→Expired: shifts past their date, jobs older than the cutoff. Runs at startup and on every IngestionWorker cycle (independent of ingestion being enabled)
- Verified: backdated job dropped off /Jobs (6→5) and was archived to Expired on the sweep
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- SubmissionGuard.PostingRateExceededAsync: max 20 new listings (shifts+jobs) per account per rolling hour, enforced in PostJob + PostShift
- Captcha + spam-name screen added to /Employer/RegisterFacility
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- CaptchaService: stateless data-protected math captcha (no Google reCAPTCHA — blocked in Iran), TTL + Persian-digit tolerant; on PostJob + PostShift
- SubmissionGuard: duplicate-position detection (facility+role+date/time for shifts, facility+role+title for jobs), spam/garbage screen on title/description, double-apply prevention
- InterestService: Apply events deduped so an applicant can't apply to the same listing twice
- Verified: wrong captcha rejected, correct publishes, duplicate + garbage blocked
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- MedjobsListingSource: crawls medjobs.ir sitemaps (ad_listing-sitemapN) → fetches ad pages → title+description → engine (dedupe/parse/validate/publish as SEO job pages). Configured in /Admin/Settings (enable + max ads/run).
- Login/register now asks 'کادر درمان' vs 'کارفرما/مرکز': new accounts get Doctor vs FacilityAdmin role; post-login routes to /Me, /Employer, or /Admin accordingly.
- Verified live: medjobs run fetched real ads into the review queue; employer signup → /Employer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Central nginx is containerized and proxies via host IP (171.22.25.73:port), not localhost → publish app on host :2569 (was 127.0.0.1)
- nginx vhost rewritten to match the monolithic config style (server blocks to paste into http{}, manual /etc/ssl/hamkadr certs, proxy_pass 171.22.25.73:2569, $connection_upgrade)
- DEPLOY.md: corrected architecture/ports, removed certbot+sites-available (use manual certs + single nginx.conf)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
docker-compose.yml HOST_PORT default → 2569; nginx vhost proxy_pass → 127.0.0.1:2569; DEPLOY.md updated. Set HOST_PORT=2569 in the ENV_FILE secret.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AppSetting gains source config: AutoIngestEnabled, IngestIntervalMinutes, Telegram/Bale/Divar enabled+channels/token/queries
- IListingSource.FetchAsync(AppSetting) — sources read config from DB, not IOptions/appsettings; sample source dev-only
- IngestionWorker reads AutoIngest+interval from DB each cycle (toggle at runtime, no redeploy)
- /Admin/Settings gets a 'منابع جمعآوری' section; removed Ingestion env/appsettings + compose env vars
- ENV_FILE shrinks to HOST_PORT + POSTGRES_* + ADMIN_PHONE (AI + sources are all in-admin); migration
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audited against working Meezi/DrSousan pipelines. Fixes:
- Single docker-compose.yml is the production stack (api + internal db); folded in docker-compose.prod.yml; dev Postgres → docker-compose.dev.yml
- Dockerfile HEALTHCHECK (bash /dev/tcp) so deploy's docker-inspect Health.Status wait works
- Naming to convention: service api, container hamkadr_api/hamkadr_db, image mirror.soroushasadi.com/hamkadr/api:${API_TAG}
- Workflow rewritten to DrSousan pattern: ci build + deploy (rollback-tag before build, pg_dump backup, stop/rm/up, docker-inspect health-wait with crash detection, scoped image prune)
- environment: block with ${VAR:-default} substitution (no hard-failing env_file); HOST_PORT; .env excluded from image context
- nginx vhost + DEPLOY.md updated to match
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Mobile nav was display:none (no navigation on phones) → now a scrollable strip on a second header row
- Fluid typography via clamp() for hero/page/section headings + spacing
- Long pay/choice labels (… یا … به انتخاب شما) now wrap in card footers instead of overflowing
- Calendar table scrolls horizontally on small screens (cal-wrap + min-width)
- Filter sidebar non-sticky/full-width on mobile; tighter small-screen padding & buttons
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>