[Demo] Add admin demo-mode toggle + generic website ingest source
CI/CD / CI · dotnet build (push) Failing after 1m40s
CI/CD / Deploy · hamkadr (push) Has been skipped

- AppSetting: DemoMode, WebsitesEnabled, WebsiteUrls
- Facility.IsDemo flag; SeedData split into SeedReferenceAsync (always)
  + SeedDemoAsync/ClearDemoAsync (idempotent, toggleable at runtime)
- WebsiteListingSource: scrape any admin-configured URL (og:title + content)
- Admin Settings: seed/clear demo card, demo-mode checkbox, website source
  fields; Program.cs seeds demo when DemoMode on (or in Development)
- EF migration DemoModeAndWebsites

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 13:43:07 +03:30
parent eae38373b9
commit 0c0449c2b9
11 changed files with 1341 additions and 149 deletions
@@ -12,6 +12,15 @@
</div>
<div class="container section" style="max-width:680px;">
@if (Model.DemoMsg is not null) { <div class="alert alert-success">@Model.DemoMsg</div> }
<div class="card card-pad" style="margin-bottom:14px;">
<h3 style="margin-top:0;">حالت نمایشی (Demo)</h3>
<p class="muted" style="font-size:13px; margin-top:0;">داده‌های نمونه‌ی تهران را برای نمایش/دمو روی سایت قرار بده یا حذف کن. (تیک «حالت نمایشی» پایین را هم بزن تا پس از هر استقرار دوباره ساخته شود.)</p>
<div style="display:flex; gap:8px; flex-wrap:wrap;">
<form method="post"><button asp-page-handler="SeedDemo" class="btn btn-outline">ساخت داده نمونه</button></form>
<form method="post" onsubmit="return confirm('همه داده‌های نمونه حذف شوند؟');"><button asp-page-handler="ClearDemo" class="btn btn-outline" style="color:var(--danger); border-color:var(--danger);">حذف داده نمونه</button></form>
</div>
</div>
@if (Model.SmsTest is not null) { <div class="alert alert-success">@Model.SmsTest</div> }
<form method="post" class="card card-pad" style="margin-bottom:14px; display:flex; gap:8px; align-items:end; flex-wrap:wrap;">
<div class="filter-group" style="margin:0; flex:1; min-width:160px;">
@@ -123,6 +132,27 @@
<p class="muted" style="font-size:12px; margin:4px 0 0;">آگهی‌های تکراری به‌صورت خودکار رد می‌شوند؛ هر اجرا فقط آگهی‌های جدید را می‌آورد.</p>
</div>
<div class="filter-group">
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
<input type="checkbox" name="WebsitesEnabled" value="true" style="width:auto;" checked="@Model.WebsitesEnabled" />
وب‌سایت‌ها (آدرس‌های دلخواه)
</label>
<label style="margin-top:6px;">آدرس صفحه‌ها (هر خط یک URL)</label>
<textarea name="WebsiteUrls" rows="3" dir="ltr" placeholder="https://example.ir/jobs/123">@Model.WebsiteUrls</textarea>
<p class="muted" style="font-size:12px; margin:4px 0 0;">موتور هر آدرس را می‌خواند و متن آگهی را استخراج می‌کند (عنوان og + بدنه محتوا). برای هر صفحه شغلی، آرشیو کانال یا آگهی طبقه‌بندی.</p>
</div>
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
<h3 style="margin-top:0;">حالت نمایشی (Demo)</h3>
<div class="filter-group">
<label style="display:flex; align-items:center; gap:8px; font-weight:700;">
<input type="checkbox" name="DemoMode" value="true" style="width:auto;" checked="@Model.DemoMode" />
حالت نمایشی فعال باشد — داده‌های نمونه پس از هر استقرار به‌صورت خودکار ساخته شوند
</label>
<p class="muted" style="font-size:12px; margin:4px 0 0;">برای ساخت/حذف فوری داده‌های نمونه از کارت بالای همین صفحه استفاده کن.</p>
</div>
<hr style="border:none; border-top:1px solid var(--line); margin:18px 0;" />
<h3 style="margin-top:0;">پیامک ورود (OTP) — کاوه‌نگار</h3>
@@ -1,3 +1,4 @@
using JobsMedical.Web.Data;
using JobsMedical.Web.Models;
using JobsMedical.Web.Services;
using JobsMedical.Web.Services.Scraping;
@@ -12,10 +13,12 @@ public class SettingsModel : PageModel
{
private readonly SettingsService _settings;
private readonly ISmsSender _sms;
public SettingsModel(SettingsService settings, ISmsSender sms)
private readonly AppDbContext _db;
public SettingsModel(SettingsService settings, ISmsSender sms, AppDbContext db)
{
_settings = settings;
_sms = sms;
_db = db;
}
[BindProperty] public IngestionMode Mode { get; set; }
@@ -48,8 +51,12 @@ public class SettingsModel : PageModel
[BindProperty] public string? VapidPrivateKey { get; set; }
[BindProperty] public string? VapidSubject { get; set; }
[BindProperty] public string? TestPhone { get; set; }
[BindProperty] public bool DemoMode { get; set; }
[BindProperty] public bool WebsitesEnabled { get; set; }
[BindProperty] public string? WebsiteUrls { get; set; }
[TempData] public string? Saved { get; set; }
[TempData] public string? SmsTest { get; set; }
[TempData] public string? DemoMsg { get; set; }
public async Task OnGetAsync()
{
@@ -78,6 +85,9 @@ public class SettingsModel : PageModel
SmsTemplate = s.SmsTemplate;
SmsSender = s.SmsSender;
NeshanMapKey = s.NeshanMapKey;
DemoMode = s.DemoMode;
WebsitesEnabled = s.WebsitesEnabled;
WebsiteUrls = s.WebsiteUrls;
PushEnabled = s.PushEnabled;
VapidPublicKey = s.VapidPublicKey;
VapidPrivateKey = s.VapidPrivateKey;
@@ -112,6 +122,9 @@ public class SettingsModel : PageModel
SmsTemplate = SmsTemplate,
SmsSender = SmsSender,
NeshanMapKey = NeshanMapKey,
DemoMode = DemoMode,
WebsitesEnabled = WebsitesEnabled,
WebsiteUrls = WebsiteUrls,
PushEnabled = PushEnabled,
VapidPublicKey = VapidPublicKey,
VapidPrivateKey = VapidPrivateKey,
@@ -121,6 +134,20 @@ public class SettingsModel : PageModel
return RedirectToPage();
}
public async Task<IActionResult> OnPostSeedDemoAsync()
{
var n = await SeedData.SeedDemoAsync(_db);
DemoMsg = n > 0 ? $"داده‌های نمونه ثبت شد ({n} مرکز + شیفت/استخدام)." : "داده‌های نمونه از قبل موجود است.";
return RedirectToPage();
}
public async Task<IActionResult> OnPostClearDemoAsync()
{
var n = await SeedData.ClearDemoAsync(_db);
DemoMsg = $"داده‌های نمونه حذف شد ({n} مرکز و آگهی‌های وابسته).";
return RedirectToPage();
}
public async Task<IActionResult> OnPostTestSmsAsync()
{
var s = await _settings.GetAsync();