Commit Graph

142 Commits

Author SHA1 Message Date
soroush.asadi 8b0b21f24d Search: Elasticsearch-style highlighted match snippets (results + typeahead)
CI/CD / CI · dotnet build (push) Successful in 6m9s
CI/CD / Deploy · hamkadr (push) Has been cancelled
- SearchHighlight.Snippet: extracts a ±70-char window around the first
  matching term and marks it (with ellipses) — the ES "highlight" fragment.
- Result cards (shift/job/talent) now show that snippet from the matched
  description/tags when a query is present, so you SEE where the term hit
  (e.g. «…دارای مدرک <mark>mmt</mark>…») instead of just the role.
- Typeahead suggestions gain a highlighted "sub" line (talent→tags,
  shift→city·specialty, job→facility·city) so matches show in the dropdown too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 21:43:50 +03:30
soroush.asadi bd8d754ee8 NuGet: drop Liara from root nuget.config too (Nexus-only everywhere)
CI/CD / CI · dotnet build (push) Successful in 9m26s
CI/CD / Deploy · hamkadr (push) Successful in 5m44s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 21:17:34 +03:30
soroush.asadi 69a630d185 CI/Docker NuGet: Nexus-only (drop Liara fallback)
NuGet loads the service index of EVERY listed source, so a 500 from the
Liara fallback aborted the whole restore (NU1301) even though Nexus was
healthy. Mirror cert chain is fixed now, so use our Nexus mirror as the
single source of truth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 21:10:56 +03:30
soroush.asadi 3d1d72ed9b ci: rerun after mirror cert fix
CI/CD / CI · dotnet build (push) Failing after 36s
CI/CD / Deploy · hamkadr (push) Has been skipped
2026-06-08 21:07:52 +03:30
soroush.asadi 36612b6bf0 CI/Docker NuGet: Nexus nuget-group primary + Liara fallback
CI/CD / CI · dotnet build (push) Failing after 42s
CI/CD / Deploy · hamkadr (push) Has been skipped
Both the CI restore (/tmp/nuget.ci.config) and the Docker image build
(nuget.docker.config) now use https://mirror.soroushasadi.com/repository/
nuget-group/ as the primary source with Liara as fallback, so a single
mirror returning 500 no longer breaks restore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 19:44:57 +03:30
soroush.asadi eb7d0f6559 Fix: search suggestions dropdown was clipped by the pill's overflow:hidden
CI/CD / CI · dotnet build (push) Failing after 14s
CI/CD / Deploy · hamkadr (push) Has been skipped
Moved overflow:hidden onto an inner .nav-search-pill so the rounded corners
still clip the input/button, but the absolutely-positioned suggestions box
(a child of the non-clipped .nav-search) is no longer hidden. Dropdown given
a readable min-width.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 17:42:07 +03:30
soroush.asadi 61afc957aa Search: fix header UI + instant typeahead (5 highlighted matches) + ranking
CI/CD / CI · dotnet build (push) Successful in 1m38s
CI/CD / Deploy · hamkadr (push) Successful in 2m6s
- Header search restyled as one clean RTL pill (input + button flush).
- Google-style autocomplete: typing ≥2 chars fetches /search/suggest and
  shows up to 5 live matches (round-robin across shifts/jobs/applicants)
  with the query highlighted, plus a «همه نتایج» link. Debounced, closes on
  outside-click/Escape.
- Search results page now RANKS by relevance (term hits in role/title/
  facility/city/tags weighted ×3, description ×1) instead of date-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:58:30 +03:30
soroush.asadi 9db4deafbc Site-wide rich search with keyword highlighting + header search box
CI/CD / CI · dotnet build (push) Successful in 1m53s
CI/CD / Deploy · hamkadr (push) Successful in 2m47s
- /Search: searches shifts, hiring openings, and applicants together via
  Postgres ILIKE (every term must match across role/city/facility/title/
  description/tags/person). Results grouped per type.
- Keyword highlighting (<mark>) extended to shift & job cards (was talent-only),
  so matches stand out everywhere.
- Persistent header search box (.nav-search) → /Search; big hero box on the
  page itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:40:26 +03:30
soroush.asadi 234bcd1f88 Polished animated contact-reveal box (shift/job/talent details)
CI/CD / CI · dotnet build (push) Successful in 3m46s
CI/CD / Deploy · hamkadr (push) Has been cancelled
Replaced the plain "interest recorded" alert with a styled .contact-reveal
card that fades/slides in and lists each channel as its own row (icon +
label + value + action button). Shift/job show facility phone + Bale;
talent shows all its ContactMethods in the same table style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:33:41 +03:30
soroush.asadi 6b657c7795 Applicants: auto-tags + deep search w/ highlight; never delete (archive instead)
CI/CD / CI · dotnet build (push) Successful in 2m1s
CI/CD / Deploy · hamkadr (push) Successful in 2m36s
- Tags: parser extracts cert/skill keywords (mmt, ICU/CCU, دیالیز, اتاق عمل,
  اورژانس, مسئول فنی, پروانه‌دار…) + role + city into TalentListing.Tags
  (+ migration); shown as chips on cards.
- Deep search on /Talent: «جستجوی عمیق» box does Postgres ILIKE across
  tags, description, person, area, role, city (every term must match);
  matches are highlighted with <mark> via SearchHighlight.
- Never delete: ShiftStatus.Archived + the admin «بایگانی گروهی» action now
  ARCHIVES aggregated posts (hidden from site, kept in DB) and leaves the
  raw crawl rows intact — a permanent archive for future analytics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:25:32 +03:30
soroush.asadi e4dc5180ad Applicants: 1→N contact methods with types (phone/email/Instagram/Telegram/Bale/site)
CI/CD / CI · dotnet build (push) Successful in 1m32s
CI/CD / Deploy · hamkadr (push) Successful in 1m31s
- ContactMethod entity (Type + Value + SortOrder) 1→N on TalentListing (+ migration).
- Parser extracts ALL contacts: multiple phones + landlines, email, and
  socials (Instagram/Telegram/Bale/WhatsApp/website) via URLs and Persian
  keyword cues; primary Phone kept for cards.
- ContactInfo helper: per-type label/icon/clickable href (tel:/mailto:/t.me/…).
- Ingestion attaches contacts to each (fanned-out) talent listing; manual
  Review re-parses to attach them + the admin-typed phone.
- Talent details renders the full contact list as buttons; falls back to the
  single phone, then the Divar source link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 11:10:19 +03:30
soroush.asadi 48760c4e83 Multi-role ads: parse all roles + fan-out publish one listing per role
CI/CD / CI · dotnet build (push) Successful in 2m16s
CI/CD / Deploy · hamkadr (push) Has been cancelled
An ad like «استخدام پرستار سالمند و کودک و همراه بیمار» names several roles;
we kept only the first. Now:
- Parser collects ALL roles (ParsedListing.RoleNames): exact taxonomy
  matches (substring-deduped so پرستار⊂پرستار سالمندان) plus synonyms
  (سالمند→پرستار سالمندان, کودک/همراه بیمار→پرستار, اتاق عمل→تکنسین اتاق عمل…),
  capped at 4.
- Ingestion publishes one Shift/Job/Talent per resolved role (AI role +
  parser roles, distinct, capped), so each role is independently
  browsable and filterable. RawListing dedupe is unchanged (one raw → N posts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 10:58:29 +03:30
soroush.asadi 13e00ec011 Validator: phone optional for applicants (publish + redirect to Divar)
CI/CD / CI · dotnet build (push) Successful in 3m10s
CI/CD / Deploy · hamkadr (push) Successful in 4m8s
A Divar applicant whose number is behind the login-gated reveal should
still publish — the detail page already links back to Divar for the phone.
Talent now scores role(40)+medical(10)=50, so role+medical alone passes
without a phone; phone just adds confidence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:32:48 +03:30
soroush.asadi 386e25c8fd Validator: discard promotional/training ads (workshops, courses)
CI/CD / CI · dotnet build (push) Has been cancelled
CI/CD / Deploy · hamkadr (push) Has been cancelled
Medical-flavored ads like «کارگاه بوتاکس و فیلر… ویژه پزشکان ۱۰٪» passed the
medical gate and got misclassified as a پزشک عمومی shift with a bogus 10%
share. Now: if a course/event/product marker is present and there's no
staffing intent (hiring/shift/availability), the item is auto-discarded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:30:23 +03:30
soroush.asadi 70c048a37b Add دندانپزشک + پرستار سالمندان roles (idempotent ensure on startup)
CI/CD / CI · dotnet build (push) Successful in 2m11s
CI/CD / Deploy · hamkadr (push) Has been cancelled
Roles were only seeded on a fresh DB, so existing deployments never got
new ones. Introduced a canonical role list + EnsureRolesAsync that runs on
every startup and inserts any missing role — so production picks up the two
new roles without a manual step. Original 7 keep their order/ids; the two
new roles are appended (sort 8-9).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:27:34 +03:30
soroush.asadi fb02c81830 Social auto-posting (phase 1): daily applicant digest to Telegram/Bale + Instagram caption
CI/CD / CI · dotnet build (push) Successful in 1m51s
CI/CD / Deploy · hamkadr (push) Successful in 2m51s
Adds a «شبکه‌های اجتماعی» admin section + scheduler that publishes a daily
«کادر آماده‌به‌کار امروز» digest:

- AppSetting: social toggles, posts-per-day, editable header/footer,
  per-channel bot token + chat id (Telegram, Bale), Instagram enable +
  extra hashtags, proxy toggle, last-posted timestamp (+ migration).
- SocialPostService: builds today's talent digest as text, posts to
  Telegram and Bale via their bot sendMessage APIs (proxy-aware), and
  produces an Instagram caption + auto hashtags (role/city based).
- SocialPostWorker: posts N times/day, evenly spaced, self-paced; reads
  settings live so it's togglable without redeploy.
- /Admin/Social: credentials + header/footer + posts/day, live preview of
  today's message, «ارسال اکنون» button, and an Instagram caption pack
  with copy button (semi-automatic — you post the image manually).
- Nav link added.

Telegram/Bale post as TEXT (per request). The Vazirmatn image card for
Instagram is phase 2 (needs SkiaSharp+HarfBuzz + a TTF font).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:20:49 +03:30
soroush.asadi 2bb8771ade Normalize ریال→تومان pricing; stop exposing crawl source (medjobs/telegram)
CI/CD / CI · dotnet build (push) Successful in 29s
CI/CD / Deploy · hamkadr (push) Successful in 42s
- Parser now reads the currency: ریال amounts (incl. «میلیون ریال» and
  numbers with no تومان unit but ≥200M) are converted to تومان (÷10), so
  «۴۰۰٬۰۰۰٬۰۰۰ ریال» shows as ۴۰٬۰۰۰٬۰۰۰ تومان instead of 400M.
- Aggregated facility fallback name no longer embeds the source
  («مرکز درمانی (از مدجابز)» → «مرکز درمانی (نامشخص)»).
- Talent details only ever names Divar as a fallback source (when the
  number couldn't be extracted); medjobs/telegram are never shown publicly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 09:05:34 +03:30
soroush.asadi 490821a637 Talent lifecycle (21-day expiry) + noindex expired job/shift details
CI/CD / CI · dotnet build (push) Successful in 2m24s
CI/CD / Deploy · hamkadr (push) Successful in 2m47s
- Talent «آماده به کار» now has its own freshness window (21 days, vs 30
  for jobs) since availability goes stale fast; archiver, browse, and home
  use TalentCutoffUtc.
- Expired/filled job openings and past/filled shifts now emit
  robots noindex so Google drops dead listings instead of keeping
  soft-404 pages. (Talent details were already noindex.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 08:59:54 +03:30
soroush.asadi f9d7c48d88 Admin settings: give each ingestion source its own card
CI/CD / CI · dotnet build (push) Successful in 2m17s
CI/CD / Deploy · hamkadr (push) Successful in 1m57s
The sources panel (Telegram/Bale/Divar/Medjobs/Websites/Proxy) ran
together as one flat list. Each is now wrapped in a bordered .source-box
with an icon + hint, so it's clear where one source's settings end and the
next begins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 08:49:27 +03:30
soroush.asadi 0622270cd2 Fix: site-wide phone on every Medjobs ad + phone mistaken for price
CI/CD / CI · dotnet build (push) Successful in 2m7s
CI/CD / Deploy · hamkadr (push) Successful in 1m59s
- HarvestPhones was run over the whole page, so Medjobs' own header/footer
  number (09101016110) was appended to every ad. Now harvest only the ad's
  description region in Medjobs + Website sources; the protected number
  still comes from the reveal call. No more duplicate number across ads.
- The amount extractor read phone digits as a Toman price
  (۹,۱۰۱,۰۱۶,۱۱۰ تومان). The parser now strips «شماره تماس…» lines and
  mobile/landline numbers before extracting money, and only accepts 6–10
  digit numbers with no leading zero (phones/ids start with 0 or are 11+).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 08:42:21 +03:30
soroush.asadi b092a5cfe5 Admin: bulk-delete published ingested posts; talent: point to source when no phone
CI/CD / CI · dotnet build (push) Successful in 1m52s
CI/CD / Deploy · hamkadr (push) Successful in 2m41s
- /Admin/Ingested: "حذف گروهی همه‌ی منتشرشده‌ها" button removes, in one
  transaction, every aggregated Shift/Job/Talent published from ingestion
  plus the approved (Normalized) raw items that produced them. Confirms
  first and reports counts. Raw rows deleted before the posts (they hold
  the FKs); DB cascade clears applications/interest events.
- Talent details: when the contact number couldn't be extracted (e.g.
  Divar's login-gated reveal), show a prominent "مشاهده شماره در دیوار/مدجابز ↗"
  link to the original ad instead of the call button.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 08:36:12 +03:30
soroush.asadi a5d6e212e2 Divar: capture post token + harvest phone from full ad detail
CI/CD / CI · dotnet build (push) Successful in 2m4s
CI/CD / Deploy · hamkadr (push) Successful in 2m18s
- Harvest now keeps each post's token, so we build a real post URL
  (divar.ir/v/{token}) instead of a generic link.
- For each post we fetch the detail JSON (posts-v2/web/{token}) and
  harvest any contact number from it — covering the very common case
  where the poster writes the phone into the ad description. Divar's
  click-to-reveal is login-gated, so this gets the in-text numbers
  without auth; fails soft (blocking/errors → skip).
- HarvestPhones hardened with digit-boundary guards so it can't grab a
  slice of a longer numeric id/timestamp inside JSON.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 08:28:37 +03:30
soroush.asadi d238888710 Medjobs: reveal hidden contact number via admin-ajax during crawl
CI/CD / CI · dotnet build (push) Successful in 1m16s
CI/CD / Deploy · hamkadr (push) Successful in 2m14s
The contact phone on medjobs.ir is loaded by JS only after clicking
«تماس با این آگهی» — it isn't in the page HTML, so scanning the markup
found nothing. We now replay that exact reveal request server-side:

- POST https://medjobs.ir/wp-admin/admin-ajax.php with
  action=isatis_protect_contact & id=<listingId> (no nonce needed),
  then harvest the tel: numbers from the returned HTML table.
- Listing id is pulled from the page via the WP shortlink (?p=ID),
  postid-/data-id, or the visible «کد آگهی» as a fallback.
- Numbers are appended to the ad text so the parser/AI capture them and
  they reach the published listing. Wrapped in try/catch so a failed
  reveal never breaks ingestion; uses the same (proxy-aware, brotli-
  decompressing) client as the page fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 08:21:24 +03:30
soroush.asadi 213af9db48 AI tag/category assignment + phone extraction from web ads
CI/CD / CI · dotnet build (push) Successful in 2m37s
CI/CD / Deploy · hamkadr (push) Successful in 1m11s
AI (when enabled, now that the server proxy is up):
- AiStructured gains phone, personName, yearsExperience, isLicensed.
- The auditor appends an authoritative output-schema to the admin prompt
  so classification stays correct even with an older stored prompt — it
  now classifies kind as shift|job|talent and extracts the contact phone
  and talent details.
- Ingestion publish prefers the AI's tags (kind/role/city/facility/phone +
  talent fields) over the heuristic parser when present.
- Default prompt updated to describe the three kinds + new fields.

Phone extraction from websites (Medjobs / generic sites), where the
number sits behind a "تماس با این آگهی" reveal:
- HtmlUtil.HarvestPhones scans the full markup for tel: links, JSON-LD
  "telephone", data-*phone* attributes, and inline Iranian mobile/landline
  numbers (Persian digits folded), normalized (mobiles 09…, landlines 0…).
- Medjobs + Website sources append harvested numbers to the ad text so the
  parser/AI capture them; manual review then prefills the phone too.
- Parser phone extraction now also captures a landline as a fallback.

Note: if a site loads the number purely via XHR (not in HTML), a
per-source reveal endpoint would be a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 08:11:14 +03:30
soroush.asadi 4e5df73cf7 Add «آماده به کار» (talent) listing type — workers offering themselves
CI/CD / CI · dotnet build (push) Successful in 1m41s
CI/CD / Deploy · hamkadr (push) Has been cancelled
Adds a third listing kind alongside Shift/Job for healthcare staff who
advertise their own availability (very common in Iranian medical
channels, e.g. "دندانپزشک آماده همکاری… ۵۰٪ تسویه"). These have no
facility; the contact phone is the key field.

- Model: TalentListing (role, person name, years, licensed, city/district,
  area note, availability, gender, comp, phone) + ListingKind.Talent +
  RawListing.LinkedTalentId + DbSet/relations/indexes + EF migration.
- Parser: detect آماده‌به‌کار/جویای کار → Kind=Talent; extract person name,
  years of experience, licensed flag, area («منطقه ۱»), phone. Facility
  name extraction now skipped for talent.
- Validator: talent path scores role + phone + medical (no facility/pay
  required).
- Ingestion auto-publish: creates a TalentListing for talent kind.
- Review (manual publish): Talent option + talent fields; publishes a
  TalentListing without a facility. Shift/Job facility now falls back to a
  shared «نامشخص / ثبت نشده» record when the ad names none — publishing
  never fails on a missing facility.
- Browse /Talent (indexable, filters: city/district/role/gender),
  details /Talent/Details (noindex — personal contact, tel: call button),
  _TalentCard, badge-talent, nav link, home section.
- Sitemap includes /Talent; robots disallows /Talent/Details. Archiver
  expires stale talent listings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 08:01:12 +03:30
soroush.asadi bdcca5e548 Redesign header menu: separate account dropdown from dashboard nav
CI/CD / CI · dotnet build (push) Successful in 1m7s
CI/CD / Deploy · hamkadr (push) Successful in 1m57s
The profile dropdown was doing three jobs at once (account actions, the
job-seeker panel menu, and the admin panel menu) and a stray inline @if
for the notification badge leaked into the markup as literal text.

- Profile dropdown is now account-only: identity card (avatar + name +
  phone), one role-aware dashboard entry, edit profile, logout. This
  removes the leaked @if and de-clutters the menu.
- Dashboard menu is centralized in _PanelNav and auto-rendered by the
  layout on every logged-in panel page (/Admin, /Me, /Employer,
  /Preferences) instead of being duplicated in the dropdown and pages.
- Drop the now-duplicate manual <partial name="_PanelNav" /> from
  Overview, Ingested, Me/Index, Employer/Index.
- CSS: identity-card (.pd-id) styles + mobile tweaks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 07:33:22 +03:30
soroush.asadi e6a796ab27 Match crawled listings to existing facilities (fuzzy) before creating new
CI/CD / CI · dotnet build (push) Successful in 1m28s
CI/CD / Deploy · hamkadr (push) Successful in 2m24s
When publishing a scraped listing we now look for a facility we already
have that is exactly or closely the same, and only create a new one when
there is no match — avoiding duplicates like «بیمارستان میلاد» vs «میلاد».

- ListingParser: extract a facility name (keyword + distinctive words) from
  the post and surface it in the parser notes.
- FacilityMatcher: Persian-aware normalization (ي/ك, ZWNJ, punctuation),
  type-word stripping for a "core" name, contains + Levenshtein similarity,
  and FindBest (same-city exact → any-city exact → same-city fuzzy → fuzzy).
- Review (manual publish): auto-select a matching facility or prefill the
  new-facility name; resolve-or-create uses fuzzy match; dropdown preselects.
- IngestionService (auto-publish): reuse FacilityMatcher against a run-wide
  facility list (grows as new ones are created) instead of exact-name only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 07:14:48 +03:30
soroush.asadi a2fc70ae57 Fix FK violation when publishing a crawled listing without a facility
CI/CD / CI · dotnet build (push) Successful in 1m31s
CI/CD / Deploy · hamkadr (push) Successful in 1m44s
OnPostPublishAsync inserted a Shift/Job with FacilityId=0 when no
facility was selected (e.g. the dropdown is empty because no facilities
exist yet), throwing FK_Shifts_Facilities_FacilityId and surfacing the
production error page.

- Resolve-or-create the facility before insert: use the picked one, else
  create an unverified Facility from a typed name (reusing same-named).
- Guard the role too; on missing facility/role redirect back with a
  Persian error message instead of 500.
- Review form: add "new facility name" input + "— none —" option +
  error alert; add .alert-error style.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 07:09:18 +03:30
soroush.asadi 5f769b0293 [Proxy] Don't track xray config.json (survives deploys); add config.json.example
CI/CD / CI · dotnet build (push) Successful in 1m55s
CI/CD / Deploy · hamkadr (push) Failing after 34s
The real Xray VPN config held credentials and was overwritten by git checkout on every deploy. Untrack it + gitignore it + ship config.json.example as the template, so the server-side config persists across redeploys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 06:45:01 +03:30
soroush.asadi da6e86fa7f [Ingest] Full results page (all statuses) + inline quick-reject in queue
CI/CD / CI · dotnet build (push) Successful in 2m13s
CI/CD / Deploy · hamkadr (push) Has been cancelled
New /Admin/Ingested page lists every crawled item with its outcome, filterable by status (همه/در صف/پرچم‌خورده/منتشرشده/ردشده) with per-status counts and a link to the published shift or the review page. Linked from the run-history header and the admin panel nav. Plus an inline ✕رد (quick-discard) button on each queue/flagged row so admins can audit without opening the review page; full accept/reject stays on /Admin/Review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 06:41:17 +03:30
soroush.asadi 3d128ea051 [Brand] Branded favicon + icon links in <head>
CI/CD / CI · dotnet build (push) Successful in 1m43s
CI/CD / Deploy · hamkadr (push) Failing after 1m35s
Replace the default ASP.NET favicon.ico with one generated from the همکادر brand icon (multi-size 16/32/48/64), add favicon-32.png, and wire <link rel=icon> (ico + png 32/192) in the layout head so browsers and Google show the brand mark.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 06:28:01 +03:30
soroush.asadi 487c7ca82f [Ingest] Persistent crawl run-log + per-source breakdown on admin queue
CI/CD / CI · dotnet build (push) Has been cancelled
CI/CD / Deploy · hamkadr (push) Has been cancelled
Each ingestion run now records an IngestionRun row (found/queued/published/flagged/spam/duplicates + a per-source detail string). Admin → صف آگهی‌ها shows a «تاریخچه جمع‌آوری» table of the last 15 runs (hover a row for the per-source breakdown), so admins can see how much each source found vs added over time. IngestionSummary gains TotalFetched. Migration: IngestionRuns table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 06:23:58 +03:30
soroush.asadi 524c66e25e [Admin] VPN/proxy + AI test buttons; fix AI JSON parse crash on null fields
CI/CD / CI · dotnet build (push) Successful in 2m41s
CI/CD / Deploy · hamkadr (push) Failing after 2m56s
Add «تست اتصال VPN/پروکسی» (reaches a filtered site through the proxy and reports connected/latency) and «تست هوش مصنوعی» (sends a sample post through the configured model and shows the verdict + extracted fields) to admin Settings. Fix OpenAiCompatibleAuditor.ParseVerdict: TryGetInt32/64 threw on null/string JSON values (the model commonly returns payAmount/sharePercent as null), which silently failed every audit — now guarded on ValueKind==Number. Verified the real OpenAI key extracts perfectly (approve / role=پرستار / city=تهران / shift=night).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 23:23:02 +03:30
soroush.asadi 0c49b89891 [AI] Route AI calls through the Xray/V2Ray proxy (reach OpenAI from Iran)
CI/CD / CI · dotnet build (push) Successful in 1m46s
CI/CD / Deploy · hamkadr (push) Failing after 1m58s
Add AiUseProxy setting + a toggle in the AI settings section. ScrapeHttpClients.ForAi(settings) returns a proxied HttpClient (reusing IngestProxyUrl, 100s timeout) when AiUseProxy is on, otherwise direct; AI-cache keys are protected from the scrape-client cleanup. OpenAiCompatibleAuditor now uses it, so the AI auditor (e.g. api.openai.com) is reachable through the same Xray sidecar that serves Telegram. Migration adds the column.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:55:07 +03:30
soroush.asadi 018c0f0286 [Ingest] Tune parser/validator for real Divar+Medjobs data
CI/CD / CI · dotnet build (push) Successful in 2m53s
CI/CD / Deploy · hamkadr (push) Failing after 2m39s
Analyzed live Divar (POST search) and Medjobs (ad_listing sitemaps) data — both are free Persian text. Tighten the medical-relevance gate (drop generic «استخدام»/«شیفت» that match retail/restaurant ads; add clinical terms: بهیار/اتاق عمل/بیهوشی/رادیولوژی/آزمایشگاه/دیالیز/فوریت/تریاژ/… ) so off-topic Divar jobs get flagged, not treated as medical. Add clinical role synonyms in the heuristic parser (بهیار/کمک‌پرستار/سالمند→پرستار, اتاق عمل→تکنسین اتاق عمل, فوریت→فوریت‌های پزشکی, آزمایشگاه→کارشناس آزمایشگاه, فوق‌تخصص→پزشک متخصص…). Result on live data: Medjobs now yields ~9/30 queue-ready healthcare listings; Divar correctly flags ~72/75 noise for manual review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:34:05 +03:30
soroush.asadi 33c13ec524 [Dashboard] Panel sub-nav menu + clearer profile button (name, not phone digit)
CI/CD / CI · dotnet build (push) Successful in 2m11s
CI/CD / Deploy · hamkadr (push) Failing after 2m37s
Logged-in panels (admin/employer/job-seeker) now show a sticky role-based dashboard menu (_PanelNav) on Employer/Index, Me/Index and Admin/Overview, with the active section highlighted — so users have an obvious menu and dashboard, not just a hidden avatar. Profile button: avatar fallback shows a 👤 glyph instead of the phone's first digit (the confusing '0'), and the desktop button now shows the user's name (or «حساب من») so it reads as a profile menu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 22:12:51 +03:30
soroush.asadi 69e4f305e9 [Nav] Add ثبت آگهی CTA, streamline menu, active-link highlight, role dashboards
CI/CD / CI · dotnet build (push) Successful in 3m26s
CI/CD / Deploy · hamkadr (push) Failing after 2m41s
Header gets a prominent accent +ثبت آگهی CTA → /Employer/Index (auth redirect handles login → register/post). Main nav trimmed to the 5 core public links (خانه/شیفت‌ها/استخدام/مراکز/تقویم); دریافت اپ + راهنما live in the footer and علاقه‌مندی‌ها in the profile menu, so the bar is far less crowded. Added active-page highlight (accent underline on desktop, soft background on mobile). Login now sends admins to /Admin/Overview (dashboard) instead of the ingestion queue; employers→/Employer/Index, job-seekers→/Me already in place.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 21:38:08 +03:30
soroush.asadi 2485173aad [Ingest] Fix Divar: use POST search API (GET was anti-bot blocked)
CI/CD / CI · dotnet build (push) Successful in 1m51s
CI/CD / Deploy · hamkadr (push) Successful in 2m8s
Divar's /v8/web-search GET returns a BLOCKING_VIEW (anti-bot), so the old source pulled nothing useful and could scrape the block message. Switch to the working POST /v8/postlist/w/search with a browser User-Agent and a city-id map (numeric id passthrough; tehran=1 default). Skip responses that are non-2xx or contain BLOCKING_VIEW so the block page is never ingested. Verified locally: fetched 25 real Tehran job posts into the review queue, 0 block messages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 21:23:36 +03:30
soroush.asadi 6af6a026a1 [SEO] JobPosting structured data, canonical/OG meta, noindex private pages, fuller sitemap
CI/CD / CI · dotnet build (push) Successful in 59s
CI/CD / Deploy · hamkadr (push) Successful in 2m27s
Strategy = Google-for-Jobs + clean indexing. Add schema.org JobPosting JSON-LD to shift & job detail pages (title, description, datePosted, validThrough, employmentType, hiringOrganization, jobLocation, baseSalary) plus Organization + WebSite JSON-LD on the home page (SeoJsonLd helper; System.Text.Json => valid, script-safe). Layout emits per-page canonical, Open Graph + Twitter cards, and applies robots noindex,nofollow to all private/applicant areas (/Admin,/Me,/Employer,/Account,/Preferences) so applicant data is never indexed. robots.txt now disallows those + /resume,/avatar,/report,/push,/notifications and points at the sitemap; sitemap.xml adds facility pages + content pages (Download/Help/Privacy/Rules/Terms).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:16:30 +03:30
soroush.asadi aa61efd46f [Applicant+Admin] Withdraw application, delete account, admin analytics dashboard
CI/CD / CI · dotnet build (push) Successful in 47s
CI/CD / Deploy · hamkadr (push) Successful in 1m28s
Applicant: 'انصراف از درخواست' on /Me removes the Apply event for that shift/job. Account: 'حذف حساب من' on /Me/Profile permanently deletes the user + cascades (profile, alerts, reviews, applications), detaches anonymous visitor history, and signs out (per privacy policy). Admin: /Admin/Analytics dashboard — totals (users, facilities/verified, open shifts/jobs, applications, reviews), 7-day deltas, and a 14-day applications bar chart; linked from Overview alongside the new نظرات moderation page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:52:49 +03:30
soroush.asadi d87afb577c [Facilities] Public facility pages + ratings & reviews
New /Facilities/Details public page: verified badge, info, Neshan map + directions, the facility's open shifts & jobs, and a complaint form; facility cards on /Facilities link to it. Ratings & reviews: Review model (1-5 stars + comment, one per user/facility, unique index, migration); logged-in users rate/review on the facility page; average + count shown in the header and the review list; admins moderate (hide/delete) at /Admin/Reviews.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:44:25 +03:30
soroush.asadi 437258294b [Infra] Persist DataProtection keys in the DB (fixes logout/antiforgery on deploy)
Add Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; AppDbContext implements IDataProtectionKeyContext with a DataProtectionKeys set; PersistKeysToDbContext + SetApplicationName(hamkadr). Now the key ring is shared across restarts/replicas, so auth cookies, antiforgery tokens and the captcha no longer break on every deploy (the root cause of the earlier admin lock-out). Migration: DataProtectionKeys table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:33:20 +03:30
soroush.asadi c46e628f6a [Profile] Show applicant avatar + resume to employers; profile-completeness nudge
CI/CD / CI · dotnet build (push) Successful in 1m33s
CI/CD / Deploy · hamkadr (push) Successful in 2m7s
Employer Listings: each applicant row now shows their avatar (or initials) and a «مشاهده رزومه» link when they uploaded one (served via /resume/{id}, already access-controlled to the receiving employer). Applicant projection avoids loading avatar/resume blobs. Me panel: a nudge banner prompts users to complete their profile (name/photo/resume) when any is missing, linking to /Me/Profile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:14:03 +03:30
soroush.asadi e633463906 [Profile] Editable profile (avatar + resume) + role-based profile dropdown menu
CI/CD / CI · dotnet build (push) Successful in 44s
CI/CD / Deploy · hamkadr (push) Successful in 57s
Every user gets a full editable profile at /Me/Profile: name, role, city, specialty/title, license, years, bio + avatar image upload + resume upload (PDF/image). Avatar/resume stored in-DB on User (migration, 5 nullable columns). Endpoints: /avatar/{id} (public) and /resume/{id} (owner, admin, or an employer who received that user's application). Nav: replaced the scattered action links with an avatar button + dropdown listing all of the user's pages by role (profile, کارجو panel, alerts, preferences, notifications; employer panel; admin panel + settings; logout) — shows the avatar image or initials; collapses into the burger menu on mobile; closes on outside-click.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:49:40 +03:30
soroush.asadi 167d263560 [Applications] Applicant pipeline: employer accept/reject + status to applicant
CI/CD / CI · dotnet build (push) Successful in 43s
CI/CD / Deploy · hamkadr (push) Successful in 43s
InterestEvent gains a Status (ApplicationStatus: Interested→Accepted/Rejected; migration, default Interested). Employer/Listings shows each applicant's status with پذیرفتن/رد buttons (ownership-checked handlers update the status and notify the applicant via bell/SSE/push linking to the listing). The کارجو panel (/Me) now shows a status badge (در انتظار بررسی / پذیرفته شد / رد شد) on each applied shift/job. Reusable _ApplicantRow partial for the employer list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:27:53 +03:30
soroush.asadi 60c1997642 [Notify] Notify the employer when someone applies to their listing
CI/CD / CI · dotnet build (push) Successful in 40s
CI/CD / Deploy · hamkadr (push) Successful in 55s
Applications were recorded but the facility owner was never pinged. NotificationService gains NotifyShiftApplicationAsync/NotifyJobApplicationAsync (look up the facility owner, notify via in-app bell + live SSE + push, linking to /Employer/Listings). InterestService fires them when a NEW Apply event is saved (after the duplicate guard, so no repeat pings; View/Save/Dismiss don't notify). No-op for admin-managed facilities with no owner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:43:52 +03:30
soroush.asadi 02d635415b [Local] Use free host ports for local stack (app 18080, db 5544)
CI/CD / CI · dotnet build (push) Successful in 48s
CI/CD / Deploy · hamkadr (push) Successful in 8s
8080/8088/5434 were occupied by other local containers; pick non-clashing ports and fix LOCAL.md encoding.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:30:11 +03:30
soroush.asadi 6f02b1a0e9 [Local] Dockerized local test stack + always-show OTP in Development
CI/CD / CI · dotnet build (push) Successful in 32s
CI/CD / Deploy · hamkadr (push) Successful in 57s
Add docker-compose.local.yml + Dockerfile.local (public MS images + Liara NuGet) to run the whole app with a throwaway Postgres in one command for local testing, plus LOCAL.md. OtpService now never calls Kavenegar in the Development environment and always returns the code so the login page shows it on screen — guarantees local logins work with no SMS. Production behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:21:47 +03:30
soroush.asadi 2170ba250c [UI] Mobile sticky action bar on shift/job details (native-app feel)
CI/CD / CI · dotnet build (push) Successful in 46s
CI/CD / Deploy · hamkadr (push) Successful in 1m6s
On mobile the action panel (CTA + map) stacked at the very bottom, so users had to scroll the whole page to act. Add a fixed bottom action bar (<=860px) with the primary «اعلام تمایل» button + a quick save, always reachable like a native app; when contact is revealed it becomes a «تماس با مرکز» tel: button. The in-aside primary CTA is hidden on mobile (.aside-apply) to avoid duplication, and pages get bottom padding (.has-action-bar) so the bar never covers content. Desktop layout unchanged (bar hidden, sidebar CTA shown).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:12:34 +03:30
soroush.asadi 86809190e7 [Map] Render real Neshan map on shift/job detail pages
CI/CD / CI · dotnet build (push) Successful in 1m6s
CI/CD / Deploy · hamkadr (push) Successful in 1m12s
The detail pages showed a 'map coming later' placeholder. Add a read-only Neshan web map (Leaflet SDK) showing the facility marker when NeshanMapKey is set, plus a 'مسیریابی در نشان' directions link; falls back to coordinates when no key. New _NeshanMap shared partial loads the SDK and inits #facmap. Shift/Job Details models now expose MapKey via SettingsService. Coordinates are emitted with InvariantCulture so the decimal point/digits don't break JS. The facility registration picker already used Neshan; this reuses the same key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:57:49 +03:30