Files
hamkadr/src/JobsMedical.Web/Models/AppSetting.cs
T
soroush.asadi 0c0449c2b9
CI/CD / CI · dotnet build (push) Failing after 1m40s
CI/CD / Deploy · hamkadr (push) Has been skipped
[Demo] Add admin demo-mode toggle + generic website ingest source
- 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>
2026-06-04 13:43:07 +03:30

104 lines
5.6 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.ComponentModel.DataAnnotations;
namespace JobsMedical.Web.Models;
/// <summary>
/// Single-row (Id=1) platform settings the admin controls at runtime — chiefly the ingestion
/// automation policy and the optional AI audit layer. Kept in the DB (not appsettings) so it's
/// editable from the admin panel without a redeploy.
/// </summary>
public class AppSetting
{
public int Id { get; set; } = 1;
// --- Ingestion automation ---
public IngestionMode Mode { get; set; } = IngestionMode.Manual;
/// <summary>In Automatic mode WITHOUT AI, listings at/above this confidence auto-publish.</summary>
public int AutoPublishMinConfidence { get; set; } = 85;
// --- AI audit layer (optional) ---
public bool AiEnabled { get; set; } = false;
/// <summary>OpenAI-compatible chat-completions endpoint (self-hosted or Iranian provider).</summary>
[MaxLength(500)] public string? AiEndpoint { get; set; }
[MaxLength(200)] public string? AiApiKey { get; set; }
[MaxLength(120)] public string? AiModel { get; set; } = "gpt-4o-mini";
/// <summary>The prompt + "framework" the AI follows to approve / reject / structure a listing.</summary>
[MaxLength(4000)]
public string AiSystemPrompt { get; set; } = DefaultPrompt;
/// <summary>If AI approves AND Mode is Automatic, publish without human review.</summary>
public bool AiAutoApprove { get; set; } = false;
// --- Channel scraping sources (configured here, NOT in env) ---
/// <summary>Run the ingestion worker on a timer.</summary>
public bool AutoIngestEnabled { get; set; } = false;
public int IngestIntervalMinutes { get; set; } = 30;
public bool TelegramEnabled { get; set; } = false;
/// <summary>Public Telegram channel usernames, one per line or comma-separated.</summary>
[MaxLength(2000)] public string? TelegramChannels { get; set; }
public bool BaleEnabled { get; set; } = false;
[MaxLength(200)] public string? BaleBotToken { get; set; }
/// <summary>Demo mode — keep the sample Tehran board seeded/visible (for showcasing).</summary>
public bool DemoMode { get; set; } = false;
public bool WebsitesEnabled { get; set; } = false;
/// <summary>Generic web pages to scrape, one URL per line.</summary>
[MaxLength(4000)] public string? WebsiteUrls { get; set; }
public bool DivarEnabled { get; set; } = false;
[MaxLength(60)] public string? DivarCity { get; set; } = "tehran";
/// <summary>Divar search terms, one per line or comma-separated.</summary>
[MaxLength(2000)] public string? DivarQueries { get; set; }
/// <summary>Scrape medjobs.ir job ads (WordPress classifieds — crawled via its sitemaps).</summary>
public bool MedjobsEnabled { get; set; } = false;
/// <summary>Max ads to fetch per ingestion run (be polite; dedupe skips already-seen).</summary>
public int MedjobsMaxAds { get; set; } = 40;
// --- SMS OTP (Kavenegar). When off, the code is shown on screen (dev only). ---
public bool SmsEnabled { get; set; } = false;
[MaxLength(200)] public string? SmsApiKey { get; set; }
/// <summary>Kavenegar verify/lookup template name (preferred OTP method in Iran).</summary>
[MaxLength(100)] public string? SmsTemplate { get; set; }
/// <summary>Sender line for plain SMS fallback when no template is set.</summary>
[MaxLength(30)] public string? SmsSender { get; set; }
/// <summary>Neshan web map.js API key — enables the click-to-pick map on the facility form
/// (Google Maps is blocked in Iran). Empty → only the "my location" button is shown.</summary>
[MaxLength(200)] public string? NeshanMapKey { get; set; }
// --- Web Push (PWA notifications). VAPID keypair; generate once with the web-push tooling. ---
public bool PushEnabled { get; set; } = false;
[MaxLength(200)] public string? VapidPublicKey { get; set; }
[MaxLength(200)] public string? VapidPrivateKey { get; set; }
[MaxLength(120)] public string? VapidSubject { get; set; } = "mailto:admin@hamkadr.ir";
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
/// <summary>Split a textarea (newline/comma separated) into trimmed non-empty items.</summary>
public static List<string> SplitList(string? s) => string.IsNullOrWhiteSpace(s)
? new()
: s.Split(new[] { '\n', '\r', ',', '،' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToList();
public const string DefaultPrompt = """
تو دستیار بررسی آگهیهای کاری حوزه درمان برای پلتفرم «همکادر» هستی.
هر آگهی خام را بخوان و تصمیم بگیر:
- approve: آگهی واقعی و مرتبط با شیفت/استخدام کادر درمان است و اطلاعات کافی دارد.
- reject: تبلیغ، اسپم، نامرتبط، یا فاقد اطلاعات حداقلی است.
- review: مرتبط است اما ناقص/مبهم و نیاز به بررسی انسانی دارد.
نقش، شهر/محله، نوع شیفت، نوع همکاری، مبلغ یا درصد سهم، و عنوان را در صورت وجود استخراج کن.
فقط با یک شیء JSON پاسخ بده با کلیدهای:
decision (approve|reject|review)، confidence (0-100)، reason (فارسی کوتاه)،
kind (shift|job)، role، city، district، shiftType (day|evening|night|oncall)،
employmentType (fulltime|parttime|contract|plan)، payAmount (عدد تومان یا null)،
sharePercent (0-100 یا null)، title، facilityName.
""";
}